2025-11-25 22:23:19 +08:00
|
|
|
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
|
|
|
|
import * as ReactDOM from 'react-dom';
|
|
|
|
|
|
import * as ReactJSXRuntime from 'react/jsx-runtime';
|
2025-11-18 14:46:51 +08:00
|
|
|
|
import { Core, createLogger, Scene } from '@esengine/ecs-framework';
|
2025-10-27 09:29:11 +08:00
|
|
|
|
import * as ECSFramework from '@esengine/ecs-framework';
|
2025-11-25 22:23:19 +08:00
|
|
|
|
|
|
|
|
|
|
// 将 React 暴露到全局,供动态加载的插件使用
|
|
|
|
|
|
// editor-runtime.js 将 React 设为 external,需要从全局获取
|
|
|
|
|
|
(window as any).React = React;
|
|
|
|
|
|
(window as any).ReactDOM = ReactDOM;
|
|
|
|
|
|
(window as any).ReactJSXRuntime = ReactJSXRuntime;
|
2025-11-18 14:46:51 +08:00
|
|
|
|
import {
|
2025-11-27 20:42:46 +08:00
|
|
|
|
PluginManager,
|
2025-11-18 14:46:51 +08:00
|
|
|
|
UIRegistry,
|
|
|
|
|
|
MessageHub,
|
|
|
|
|
|
EntityStoreService,
|
|
|
|
|
|
ComponentRegistry,
|
|
|
|
|
|
LocaleService,
|
|
|
|
|
|
LogService,
|
|
|
|
|
|
SettingsRegistry,
|
|
|
|
|
|
SceneManagerService,
|
|
|
|
|
|
ProjectService,
|
|
|
|
|
|
CompilerRegistry,
|
2025-11-25 22:23:19 +08:00
|
|
|
|
ICompilerRegistry,
|
2025-11-18 14:46:51 +08:00
|
|
|
|
InspectorRegistry,
|
2025-11-21 10:03:18 +08:00
|
|
|
|
INotification,
|
|
|
|
|
|
CommandManager
|
2025-11-18 14:46:51 +08:00
|
|
|
|
} from '@esengine/editor-core';
|
|
|
|
|
|
import type { IDialogExtended } from './services/TauriDialogService';
|
2025-10-27 09:29:11 +08:00
|
|
|
|
import { GlobalBlackboardService } from '@esengine/behavior-tree';
|
2025-11-18 14:46:51 +08:00
|
|
|
|
import { ServiceRegistry, PluginInstaller, useDialogStore } from './app/managers';
|
2025-10-15 09:34:44 +08:00
|
|
|
|
import { StartupPage } from './components/StartupPage';
|
2025-11-23 21:45:10 +08:00
|
|
|
|
import { ProjectCreationWizard } from './components/ProjectCreationWizard';
|
2025-10-14 23:31:09 +08:00
|
|
|
|
import { SceneHierarchy } from './components/SceneHierarchy';
|
2025-11-21 10:03:18 +08:00
|
|
|
|
import { Inspector } from './components/inspectors/Inspector';
|
2025-10-15 09:34:44 +08:00
|
|
|
|
import { AssetBrowser } from './components/AssetBrowser';
|
2025-10-15 17:21:59 +08:00
|
|
|
|
import { ConsolePanel } from './components/ConsolePanel';
|
2025-11-21 10:03:18 +08:00
|
|
|
|
import { Viewport } from './components/Viewport';
|
2025-10-15 22:30:49 +08:00
|
|
|
|
import { ProfilerWindow } from './components/ProfilerWindow';
|
|
|
|
|
|
import { PortManager } from './components/PortManager';
|
2025-10-16 13:07:19 +08:00
|
|
|
|
import { SettingsWindow } from './components/SettingsWindow';
|
2025-10-16 22:26:50 +08:00
|
|
|
|
import { AboutDialog } from './components/AboutDialog';
|
2025-10-17 18:13:31 +08:00
|
|
|
|
import { ErrorDialog } from './components/ErrorDialog';
|
|
|
|
|
|
import { ConfirmDialog } from './components/ConfirmDialog';
|
2025-10-28 11:45:35 +08:00
|
|
|
|
import { PluginGeneratorWindow } from './components/PluginGeneratorWindow';
|
2025-11-18 14:46:51 +08:00
|
|
|
|
import { ToastProvider, useToast } from './components/Toast';
|
2025-10-15 18:24:13 +08:00
|
|
|
|
import { MenuBar } from './components/MenuBar';
|
2025-10-27 09:29:11 +08:00
|
|
|
|
import { FlexLayoutDockContainer, FlexDockPanel } from './components/FlexLayoutDockContainer';
|
2025-10-14 22:53:26 +08:00
|
|
|
|
import { TauriAPI } from './api/tauri';
|
2025-10-16 17:10:22 +08:00
|
|
|
|
import { SettingsService } from './services/SettingsService';
|
2025-10-28 11:45:35 +08:00
|
|
|
|
import { PluginLoader } from './services/PluginLoader';
|
2025-11-27 20:42:46 +08:00
|
|
|
|
import { EngineService } from './services/EngineService';
|
2025-11-18 14:46:51 +08:00
|
|
|
|
import { CompilerConfigDialog } from './components/CompilerConfigDialog';
|
2025-10-16 22:26:50 +08:00
|
|
|
|
import { checkForUpdatesOnStartup } from './utils/updater';
|
2025-10-14 23:56:54 +08:00
|
|
|
|
import { useLocale } from './hooks/useLocale';
|
|
|
|
|
|
import { en, zh } from './locales';
|
2025-11-23 21:45:10 +08:00
|
|
|
|
import type { Locale } from '@esengine/editor-core';
|
|
|
|
|
|
import { Loader2, Globe, ChevronDown } from 'lucide-react';
|
2025-10-14 22:53:26 +08:00
|
|
|
|
import './styles/App.css';
|
|
|
|
|
|
|
2025-10-15 00:15:12 +08:00
|
|
|
|
const coreInstance = Core.create({ debug: true });
|
2025-10-14 23:56:54 +08:00
|
|
|
|
|
|
|
|
|
|
const localeService = new LocaleService();
|
|
|
|
|
|
localeService.registerTranslations('en', en);
|
|
|
|
|
|
localeService.registerTranslations('zh', zh);
|
|
|
|
|
|
Core.services.registerInstance(LocaleService, localeService);
|
|
|
|
|
|
|
2025-10-27 09:29:11 +08:00
|
|
|
|
Core.services.registerSingleton(GlobalBlackboardService);
|
2025-11-18 14:46:51 +08:00
|
|
|
|
Core.services.registerSingleton(CompilerRegistry);
|
2025-10-27 09:29:11 +08:00
|
|
|
|
|
2025-11-25 22:23:19 +08:00
|
|
|
|
// 在 CompilerRegistry 实例化后,也用 Symbol 注册,用于跨包插件访问
|
|
|
|
|
|
// 注意:registerSingleton 会延迟实例化,所以需要在第一次使用后再注册 Symbol
|
|
|
|
|
|
const compilerRegistryInstance = Core.services.resolve(CompilerRegistry);
|
|
|
|
|
|
Core.services.registerInstance(ICompilerRegistry, compilerRegistryInstance);
|
|
|
|
|
|
|
2025-11-04 23:53:26 +08:00
|
|
|
|
const logger = createLogger('App');
|
|
|
|
|
|
|
2025-10-14 22:53:26 +08:00
|
|
|
|
function App() {
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const initRef = useRef(false);
|
2025-11-18 14:46:51 +08:00
|
|
|
|
const [pluginLoader] = useState(() => new PluginLoader());
|
|
|
|
|
|
const { showToast, hideToast } = useToast();
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const [initialized, setInitialized] = useState(false);
|
|
|
|
|
|
const [projectLoaded, setProjectLoaded] = useState(false);
|
|
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
|
const [loadingMessage, setLoadingMessage] = useState('');
|
|
|
|
|
|
const [currentProjectPath, setCurrentProjectPath] = useState<string | null>(null);
|
2025-11-27 20:42:46 +08:00
|
|
|
|
const [pluginManager, setPluginManager] = useState<PluginManager | null>(null);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const [entityStore, setEntityStore] = useState<EntityStoreService | null>(null);
|
|
|
|
|
|
const [messageHub, setMessageHub] = useState<MessageHub | null>(null);
|
2025-11-18 14:46:51 +08:00
|
|
|
|
const [inspectorRegistry, setInspectorRegistry] = useState<InspectorRegistry | null>(null);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const [logService, setLogService] = useState<LogService | null>(null);
|
|
|
|
|
|
const [uiRegistry, setUiRegistry] = useState<UIRegistry | null>(null);
|
|
|
|
|
|
const [settingsRegistry, setSettingsRegistry] = useState<SettingsRegistry | null>(null);
|
|
|
|
|
|
const [sceneManager, setSceneManager] = useState<SceneManagerService | null>(null);
|
2025-11-18 14:46:51 +08:00
|
|
|
|
const [notification, setNotification] = useState<INotification | null>(null);
|
|
|
|
|
|
const [dialog, setDialog] = useState<IDialogExtended | null>(null);
|
2025-11-21 10:03:18 +08:00
|
|
|
|
const [commandManager] = useState(() => new CommandManager());
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const { t, locale, changeLocale } = useLocale();
|
2025-11-18 14:46:51 +08:00
|
|
|
|
|
|
|
|
|
|
// 同步 locale 到 TauriDialogService
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (dialog) {
|
|
|
|
|
|
dialog.setLocale(locale);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [locale, dialog]);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const [status, setStatus] = useState(t('header.status.initializing'));
|
|
|
|
|
|
const [panels, setPanels] = useState<FlexDockPanel[]>([]);
|
|
|
|
|
|
const [pluginUpdateTrigger, setPluginUpdateTrigger] = useState(0);
|
|
|
|
|
|
const [isRemoteConnected, setIsRemoteConnected] = useState(false);
|
|
|
|
|
|
const [isProfilerMode, setIsProfilerMode] = useState(false);
|
2025-11-23 21:45:10 +08:00
|
|
|
|
const [showProjectWizard, setShowProjectWizard] = useState(false);
|
2025-11-18 14:46:51 +08:00
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
showProfiler, setShowProfiler,
|
|
|
|
|
|
showPortManager, setShowPortManager,
|
|
|
|
|
|
showSettings, setShowSettings,
|
|
|
|
|
|
showAbout, setShowAbout,
|
|
|
|
|
|
showPluginGenerator, setShowPluginGenerator,
|
|
|
|
|
|
errorDialog, setErrorDialog,
|
|
|
|
|
|
confirmDialog, setConfirmDialog
|
|
|
|
|
|
} = useDialogStore();
|
2025-11-27 20:42:46 +08:00
|
|
|
|
const [settingsInitialCategory, setSettingsInitialCategory] = useState<string | undefined>(undefined);
|
2025-11-04 18:29:28 +08:00
|
|
|
|
const [activeDynamicPanels, setActiveDynamicPanels] = useState<string[]>([]);
|
2025-11-04 23:53:26 +08:00
|
|
|
|
const [activePanelId, setActivePanelId] = useState<string | undefined>(undefined);
|
|
|
|
|
|
const [dynamicPanelTitles, setDynamicPanelTitles] = useState<Map<string, string>>(new Map());
|
|
|
|
|
|
const [isEditorFullscreen, setIsEditorFullscreen] = useState(false);
|
2025-11-18 14:46:51 +08:00
|
|
|
|
const [compilerDialog, setCompilerDialog] = useState<{
|
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
|
compilerId: string;
|
|
|
|
|
|
currentFileName?: string;
|
|
|
|
|
|
}>({ isOpen: false, compilerId: '' });
|
2025-11-23 21:45:10 +08:00
|
|
|
|
const [showLocaleMemu, setShowLocaleMenu] = useState(false);
|
|
|
|
|
|
const localeMenuRef = useRef<HTMLDivElement>(null);
|
2025-10-14 22:53:26 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
useEffect(() => {
|
2025-11-03 21:22:16 +08:00
|
|
|
|
// 禁用默认右键菜单
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const handleContextMenu = (e: MouseEvent) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
};
|
2025-10-15 20:23:55 +08:00
|
|
|
|
|
2025-11-23 21:45:10 +08:00
|
|
|
|
document.addEventListener('contextmenu', handleContextMenu);
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
document.removeEventListener('contextmenu', handleContextMenu);
|
|
|
|
|
|
};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 语言菜单点击外部关闭
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleClickOutside = (e: MouseEvent) => {
|
|
|
|
|
|
if (localeMenuRef.current && !localeMenuRef.current.contains(e.target as Node)) {
|
|
|
|
|
|
setShowLocaleMenu(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
|
|
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 快捷键监听
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const handleKeyDown = async (e: KeyboardEvent) => {
|
|
|
|
|
|
if (e.ctrlKey || e.metaKey) {
|
|
|
|
|
|
switch (e.key.toLowerCase()) {
|
|
|
|
|
|
case 's':
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
if (sceneManager) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await sceneManager.saveScene();
|
|
|
|
|
|
const sceneState = sceneManager.getSceneState();
|
|
|
|
|
|
showToast(locale === 'zh' ? `已保存场景: ${sceneState.sceneName}` : `Scene saved: ${sceneState.sceneName}`, 'success');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to save scene:', error);
|
|
|
|
|
|
showToast(locale === 'zh' ? '保存场景失败' : 'Failed to save scene', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'r':
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
handleReloadPlugins();
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2025-11-18 14:46:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('keydown', handleKeyDown);
|
2025-10-15 20:23:55 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
return () => {
|
2025-11-18 14:46:51 +08:00
|
|
|
|
document.removeEventListener('keydown', handleKeyDown);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
};
|
2025-11-23 21:45:10 +08:00
|
|
|
|
}, [sceneManager, locale, currentProjectPath, pluginManager]);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (messageHub) {
|
|
|
|
|
|
const unsubscribeEnabled = messageHub.subscribe('plugin:enabled', () => {
|
|
|
|
|
|
setPluginUpdateTrigger((prev) => prev + 1);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const unsubscribeDisabled = messageHub.subscribe('plugin:disabled', () => {
|
|
|
|
|
|
setPluginUpdateTrigger((prev) => prev + 1);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-18 14:46:51 +08:00
|
|
|
|
const unsubscribeNotification = messageHub.subscribe('notification:show', (notification: { message: string; type: 'success' | 'error' | 'warning' | 'info'; timestamp: number }) => {
|
|
|
|
|
|
if (notification && notification.message) {
|
|
|
|
|
|
showToast(notification.message, notification.type);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
return () => {
|
|
|
|
|
|
unsubscribeEnabled();
|
|
|
|
|
|
unsubscribeDisabled();
|
2025-11-18 14:46:51 +08:00
|
|
|
|
unsubscribeNotification();
|
2025-11-02 23:50:41 +08:00
|
|
|
|
};
|
2025-10-15 23:24:13 +08:00
|
|
|
|
}
|
2025-11-18 14:46:51 +08:00
|
|
|
|
}, [messageHub, showToast]);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
|
|
|
|
|
|
// 监听远程连接状态
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const checkConnection = () => {
|
|
|
|
|
|
const profilerService = (window as any).__PROFILER_SERVICE__;
|
2025-11-18 14:46:51 +08:00
|
|
|
|
const connected = profilerService && profilerService.isConnected();
|
|
|
|
|
|
|
|
|
|
|
|
setIsRemoteConnected((prevConnected) => {
|
|
|
|
|
|
if (connected !== prevConnected) {
|
|
|
|
|
|
// 状态发生变化
|
|
|
|
|
|
if (connected) {
|
|
|
|
|
|
setStatus(t('header.status.remoteConnected'));
|
2025-11-02 23:50:41 +08:00
|
|
|
|
} else {
|
2025-11-18 14:46:51 +08:00
|
|
|
|
if (projectLoaded) {
|
|
|
|
|
|
const componentRegistry = Core.services.resolve(ComponentRegistry);
|
|
|
|
|
|
const componentCount = componentRegistry?.getAllComponents().length || 0;
|
|
|
|
|
|
setStatus(t('header.status.projectOpened') + (componentCount > 0 ? ` (${componentCount} components registered)` : ''));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setStatus(t('header.status.ready'));
|
|
|
|
|
|
}
|
2025-11-02 23:50:41 +08:00
|
|
|
|
}
|
2025-11-18 14:46:51 +08:00
|
|
|
|
return connected;
|
2025-11-02 23:50:41 +08:00
|
|
|
|
}
|
2025-11-18 14:46:51 +08:00
|
|
|
|
return prevConnected;
|
|
|
|
|
|
});
|
2025-11-02 23:50:41 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const interval = setInterval(checkConnection, 1000);
|
|
|
|
|
|
|
|
|
|
|
|
return () => clearInterval(interval);
|
2025-11-18 14:46:51 +08:00
|
|
|
|
}, [projectLoaded, t]);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const initializeEditor = async () => {
|
|
|
|
|
|
// 使用 ref 防止 React StrictMode 的双重调用
|
|
|
|
|
|
if (initRef.current) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
initRef.current = true;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
(window as any).__ECS_FRAMEWORK__ = ECSFramework;
|
|
|
|
|
|
|
|
|
|
|
|
const editorScene = new Scene();
|
|
|
|
|
|
Core.setScene(editorScene);
|
|
|
|
|
|
|
2025-11-18 14:46:51 +08:00
|
|
|
|
const serviceRegistry = new ServiceRegistry();
|
|
|
|
|
|
const services = serviceRegistry.registerAllServices(coreInstance);
|
|
|
|
|
|
|
|
|
|
|
|
serviceRegistry.setupRemoteLogListener(services.logService);
|
|
|
|
|
|
|
|
|
|
|
|
const pluginInstaller = new PluginInstaller();
|
|
|
|
|
|
await pluginInstaller.installBuiltinPlugins(services.pluginManager);
|
|
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
// 初始化编辑器模块(安装设置、面板等)
|
|
|
|
|
|
await services.pluginManager.initializeEditor(Core.services);
|
|
|
|
|
|
|
2025-11-18 14:46:51 +08:00
|
|
|
|
services.notification.setCallbacks(showToast, hideToast);
|
|
|
|
|
|
(services.dialog as IDialogExtended).setConfirmCallback(setConfirmDialog);
|
|
|
|
|
|
|
|
|
|
|
|
services.messageHub.subscribe('ui:openWindow', (data: any) => {
|
|
|
|
|
|
const { windowId } = data;
|
2025-11-04 18:29:28 +08:00
|
|
|
|
|
|
|
|
|
|
if (windowId === 'profiler') {
|
2025-11-02 23:50:41 +08:00
|
|
|
|
setShowProfiler(true);
|
2025-11-04 18:29:28 +08:00
|
|
|
|
} else if (windowId === 'pluginManager') {
|
2025-11-27 20:42:46 +08:00
|
|
|
|
// 插件管理现在整合到设置窗口中
|
|
|
|
|
|
setSettingsInitialCategory('plugins');
|
|
|
|
|
|
setShowSettings(true);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
setInitialized(true);
|
2025-11-18 14:46:51 +08:00
|
|
|
|
setPluginManager(services.pluginManager);
|
|
|
|
|
|
setEntityStore(services.entityStore);
|
|
|
|
|
|
setMessageHub(services.messageHub);
|
|
|
|
|
|
setInspectorRegistry(services.inspectorRegistry);
|
|
|
|
|
|
setLogService(services.logService);
|
|
|
|
|
|
setUiRegistry(services.uiRegistry);
|
|
|
|
|
|
setSettingsRegistry(services.settingsRegistry);
|
|
|
|
|
|
setSceneManager(services.sceneManager);
|
|
|
|
|
|
setNotification(services.notification);
|
|
|
|
|
|
setDialog(services.dialog as IDialogExtended);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
setStatus(t('header.status.ready'));
|
|
|
|
|
|
|
|
|
|
|
|
// Check for updates on startup (after 3 seconds)
|
|
|
|
|
|
checkForUpdatesOnStartup();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to initialize editor:', error);
|
|
|
|
|
|
setStatus(t('header.status.failed'));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
initializeEditor();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-11-04 18:29:28 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!messageHub) return;
|
|
|
|
|
|
|
|
|
|
|
|
const unsubscribe = messageHub.subscribe('dynamic-panel:open', (data: any) => {
|
2025-11-04 23:53:26 +08:00
|
|
|
|
const { panelId, title } = data;
|
|
|
|
|
|
logger.info('Opening dynamic panel:', panelId, 'with title:', title);
|
|
|
|
|
|
setActiveDynamicPanels((prev) => {
|
|
|
|
|
|
const newPanels = prev.includes(panelId) ? prev : [...prev, panelId];
|
|
|
|
|
|
return newPanels;
|
2025-11-04 18:29:28 +08:00
|
|
|
|
});
|
2025-11-04 23:53:26 +08:00
|
|
|
|
setActivePanelId(panelId);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新动态面板标题
|
|
|
|
|
|
if (title) {
|
|
|
|
|
|
setDynamicPanelTitles((prev) => {
|
|
|
|
|
|
const newTitles = new Map(prev);
|
|
|
|
|
|
newTitles.set(panelId, title);
|
|
|
|
|
|
return newTitles;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return () => unsubscribe?.();
|
|
|
|
|
|
}, [messageHub]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!messageHub) return;
|
|
|
|
|
|
|
|
|
|
|
|
const unsubscribe = messageHub.subscribe('editor:fullscreen', (data: any) => {
|
|
|
|
|
|
const { fullscreen } = data;
|
|
|
|
|
|
logger.info('Editor fullscreen state changed:', fullscreen);
|
|
|
|
|
|
setIsEditorFullscreen(fullscreen);
|
2025-11-04 18:29:28 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return () => unsubscribe?.();
|
|
|
|
|
|
}, [messageHub]);
|
|
|
|
|
|
|
2025-11-18 14:46:51 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!messageHub) return;
|
|
|
|
|
|
|
|
|
|
|
|
const unsubscribe = messageHub.subscribe('compiler:open-dialog', (data: {
|
|
|
|
|
|
compilerId: string;
|
|
|
|
|
|
currentFileName?: string;
|
|
|
|
|
|
projectPath?: string;
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
logger.info('Opening compiler dialog:', data.compilerId);
|
|
|
|
|
|
setCompilerDialog({
|
|
|
|
|
|
isOpen: true,
|
|
|
|
|
|
compilerId: data.compilerId,
|
|
|
|
|
|
currentFileName: data.currentFileName
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return () => unsubscribe?.();
|
|
|
|
|
|
}, [messageHub]);
|
|
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const handleOpenRecentProject = async (projectPath: string) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
setIsLoading(true);
|
2025-11-27 20:42:46 +08:00
|
|
|
|
setLoadingMessage(locale === 'zh' ? '步骤 1/3: 打开项目配置...' : 'Step 1/3: Opening project config...');
|
2025-11-02 23:50:41 +08:00
|
|
|
|
|
|
|
|
|
|
const projectService = Core.services.resolve(ProjectService);
|
|
|
|
|
|
|
|
|
|
|
|
if (!projectService) {
|
|
|
|
|
|
console.error('Required services not available');
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await projectService.openProject(projectPath);
|
|
|
|
|
|
|
2025-11-25 22:23:19 +08:00
|
|
|
|
// 设置 Tauri project:// 协议的基础路径(用于加载插件等项目文件)
|
|
|
|
|
|
await TauriAPI.setProjectBasePath(projectPath);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
const settings = SettingsService.getInstance();
|
|
|
|
|
|
settings.addRecentProject(projectPath);
|
|
|
|
|
|
|
|
|
|
|
|
setCurrentProjectPath(projectPath);
|
|
|
|
|
|
// 设置 projectLoaded 为 true,触发主界面渲染(包括 Viewport)
|
|
|
|
|
|
setProjectLoaded(true);
|
|
|
|
|
|
|
|
|
|
|
|
// 等待引擎初始化完成(Viewport 渲染后会触发引擎初始化)
|
|
|
|
|
|
setLoadingMessage(locale === 'zh' ? '步骤 2/3: 初始化引擎和模块...' : 'Step 2/3: Initializing engine and modules...');
|
|
|
|
|
|
const engineService = EngineService.getInstance();
|
|
|
|
|
|
|
|
|
|
|
|
// 等待引擎初始化(最多等待 30 秒,因为需要等待 Viewport 渲染)
|
|
|
|
|
|
const engineReady = await engineService.waitForInitialization(30000);
|
|
|
|
|
|
if (!engineReady) {
|
|
|
|
|
|
throw new Error(locale === 'zh' ? '引擎初始化超时' : 'Engine initialization timeout');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化模块系统(所有插件的 runtimeModule 会在 PluginManager 安装时自动注册)
|
|
|
|
|
|
await engineService.initializeModuleSystems();
|
|
|
|
|
|
|
|
|
|
|
|
// 应用项目的 UI 设计分辨率
|
|
|
|
|
|
// Apply project's UI design resolution
|
|
|
|
|
|
const uiResolution = projectService.getUIDesignResolution();
|
|
|
|
|
|
engineService.setUICanvasSize(uiResolution.width, uiResolution.height);
|
|
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
setStatus(t('header.status.projectOpened'));
|
|
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
setLoadingMessage(locale === 'zh' ? '步骤 3/3: 初始化场景...' : 'Step 3/3: Initializing scene...');
|
2025-11-02 23:50:41 +08:00
|
|
|
|
|
|
|
|
|
|
const sceneManagerService = Core.services.resolve(SceneManagerService);
|
2025-11-25 22:23:19 +08:00
|
|
|
|
if (sceneManagerService) {
|
|
|
|
|
|
await sceneManagerService.newScene();
|
2025-11-02 23:50:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (pluginManager) {
|
|
|
|
|
|
setLoadingMessage(locale === 'zh' ? '加载项目插件...' : 'Loading project plugins...');
|
2025-11-18 14:46:51 +08:00
|
|
|
|
await pluginLoader.loadProjectPlugins(projectPath, pluginManager);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to open project:', error);
|
|
|
|
|
|
setStatus(t('header.status.failed'));
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
|
|
|
|
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
|
|
|
|
setErrorDialog({
|
|
|
|
|
|
title: locale === 'zh' ? '打开项目失败' : 'Failed to Open Project',
|
|
|
|
|
|
message: locale === 'zh'
|
|
|
|
|
|
? `无法打开项目:\n${errorMessage}`
|
|
|
|
|
|
: `Failed to open project:\n${errorMessage}`
|
|
|
|
|
|
});
|
2025-10-15 23:24:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const handleOpenProject = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const projectPath = await TauriAPI.openProjectDialog();
|
|
|
|
|
|
if (!projectPath) return;
|
2025-10-16 22:26:50 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
await handleOpenRecentProject(projectPath);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to open project dialog:', error);
|
|
|
|
|
|
}
|
2025-10-14 22:53:26 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-23 21:45:10 +08:00
|
|
|
|
const handleCreateProject = () => {
|
|
|
|
|
|
setShowProjectWizard(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleCreateProjectFromWizard = async (projectName: string, projectPath: string, _templateId: string) => {
|
|
|
|
|
|
const fullProjectPath = `${projectPath}\\${projectName}`;
|
2025-10-14 22:53:26 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
try {
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
setLoadingMessage(locale === 'zh' ? '正在创建项目...' : 'Creating project...');
|
|
|
|
|
|
|
|
|
|
|
|
const projectService = Core.services.resolve(ProjectService);
|
|
|
|
|
|
if (!projectService) {
|
|
|
|
|
|
console.error('ProjectService not available');
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
setErrorDialog({
|
|
|
|
|
|
title: locale === 'zh' ? '创建项目失败' : 'Failed to Create Project',
|
|
|
|
|
|
message: locale === 'zh' ? '项目服务不可用,请重启编辑器' : 'Project service is not available. Please restart the editor.'
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-15 00:40:27 +08:00
|
|
|
|
|
2025-11-23 21:45:10 +08:00
|
|
|
|
await projectService.createProject(fullProjectPath);
|
2025-10-15 00:40:27 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
setLoadingMessage(locale === 'zh' ? '项目创建成功,正在打开...' : 'Project created, opening...');
|
2025-10-15 17:15:05 +08:00
|
|
|
|
|
2025-11-23 21:45:10 +08:00
|
|
|
|
await handleOpenRecentProject(fullProjectPath);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to create project:', error);
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
|
|
|
|
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
|
|
|
|
|
2025-11-23 21:45:10 +08:00
|
|
|
|
if (errorMessage.includes('already exists')) {
|
2025-11-02 23:50:41 +08:00
|
|
|
|
setConfirmDialog({
|
|
|
|
|
|
title: locale === 'zh' ? '项目已存在' : 'Project Already Exists',
|
|
|
|
|
|
message: locale === 'zh'
|
|
|
|
|
|
? '该目录下已存在 ECS 项目,是否要打开该项目?'
|
|
|
|
|
|
: 'An ECS project already exists in this directory. Do you want to open it?',
|
|
|
|
|
|
confirmText: locale === 'zh' ? '打开项目' : 'Open Project',
|
|
|
|
|
|
cancelText: locale === 'zh' ? '取消' : 'Cancel',
|
|
|
|
|
|
onConfirm: () => {
|
|
|
|
|
|
setConfirmDialog(null);
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
|
setLoadingMessage(locale === 'zh' ? '正在打开项目...' : 'Opening project...');
|
2025-11-23 21:45:10 +08:00
|
|
|
|
handleOpenRecentProject(fullProjectPath).catch((err) => {
|
2025-11-02 23:50:41 +08:00
|
|
|
|
console.error('Failed to open project:', err);
|
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
|
setErrorDialog({
|
|
|
|
|
|
title: locale === 'zh' ? '打开项目失败' : 'Failed to Open Project',
|
|
|
|
|
|
message: locale === 'zh'
|
|
|
|
|
|
? `无法打开项目:\n${err instanceof Error ? err.message : String(err)}`
|
|
|
|
|
|
: `Failed to open project:\n${err instanceof Error ? err.message : String(err)}`
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setStatus(locale === 'zh' ? '创建项目失败' : 'Failed to create project');
|
|
|
|
|
|
setErrorDialog({
|
|
|
|
|
|
title: locale === 'zh' ? '创建项目失败' : 'Failed to Create Project',
|
|
|
|
|
|
message: locale === 'zh'
|
|
|
|
|
|
? `无法创建项目:\n${errorMessage}`
|
|
|
|
|
|
: `Failed to create project:\n${errorMessage}`
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-10-15 17:15:05 +08:00
|
|
|
|
|
2025-11-23 21:45:10 +08:00
|
|
|
|
const handleBrowseProjectPath = async (): Promise<string | null> => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const path = await TauriAPI.openProjectDialog();
|
|
|
|
|
|
return path || null;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to browse path:', error);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const handleProfilerMode = async () => {
|
|
|
|
|
|
setIsProfilerMode(true);
|
2025-11-23 21:45:10 +08:00
|
|
|
|
setIsRemoteConnected(true);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
setProjectLoaded(true);
|
|
|
|
|
|
setStatus(t('header.status.profilerMode') || 'Profiler Mode - Waiting for connection...');
|
|
|
|
|
|
};
|
2025-10-15 00:40:27 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const handleNewScene = async () => {
|
|
|
|
|
|
if (!sceneManager) {
|
|
|
|
|
|
console.error('SceneManagerService not available');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-15 00:40:27 +08:00
|
|
|
|
|
2025-10-17 18:13:31 +08:00
|
|
|
|
try {
|
2025-11-02 23:50:41 +08:00
|
|
|
|
await sceneManager.newScene();
|
|
|
|
|
|
setStatus(locale === 'zh' ? '已创建新场景' : 'New scene created');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to create new scene:', error);
|
|
|
|
|
|
setStatus(locale === 'zh' ? '创建场景失败' : 'Failed to create scene');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-10-15 00:40:27 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const handleOpenScene = async () => {
|
|
|
|
|
|
if (!sceneManager) {
|
|
|
|
|
|
console.error('SceneManagerService not available');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-15 17:15:05 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
try {
|
|
|
|
|
|
await sceneManager.openScene();
|
|
|
|
|
|
const sceneState = sceneManager.getSceneState();
|
|
|
|
|
|
setStatus(locale === 'zh' ? `已打开场景: ${sceneState.sceneName}` : `Scene opened: ${sceneState.sceneName}`);
|
2025-10-17 18:13:31 +08:00
|
|
|
|
} catch (error) {
|
2025-11-02 23:50:41 +08:00
|
|
|
|
console.error('Failed to open scene:', error);
|
|
|
|
|
|
setStatus(locale === 'zh' ? '打开场景失败' : 'Failed to open scene');
|
2025-10-17 18:13:31 +08:00
|
|
|
|
}
|
2025-11-02 23:50:41 +08:00
|
|
|
|
};
|
2025-10-15 00:23:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const handleOpenSceneByPath = useCallback(async (scenePath: string) => {
|
|
|
|
|
|
if (!sceneManager) {
|
|
|
|
|
|
console.error('SceneManagerService not available');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await sceneManager.openScene(scenePath);
|
|
|
|
|
|
const sceneState = sceneManager.getSceneState();
|
|
|
|
|
|
setStatus(locale === 'zh' ? `已打开场景: ${sceneState.sceneName}` : `Scene opened: ${sceneState.sceneName}`);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to open scene:', error);
|
|
|
|
|
|
setStatus(locale === 'zh' ? '打开场景失败' : 'Failed to open scene');
|
|
|
|
|
|
setErrorDialog({
|
|
|
|
|
|
title: locale === 'zh' ? '打开场景失败' : 'Failed to Open Scene',
|
2025-10-17 18:13:31 +08:00
|
|
|
|
message: locale === 'zh'
|
2025-11-02 23:50:41 +08:00
|
|
|
|
? `无法打开场景:\n${error instanceof Error ? error.message : String(error)}`
|
|
|
|
|
|
: `Failed to open scene:\n${error instanceof Error ? error.message : String(error)}`
|
2025-10-17 18:13:31 +08:00
|
|
|
|
});
|
2025-11-02 23:50:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
}, [sceneManager, locale]);
|
2025-10-17 18:13:31 +08:00
|
|
|
|
|
2025-10-15 09:34:44 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const handleSaveScene = async () => {
|
|
|
|
|
|
if (!sceneManager) {
|
|
|
|
|
|
console.error('SceneManagerService not available');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-15 09:34:44 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
try {
|
|
|
|
|
|
await sceneManager.saveScene();
|
|
|
|
|
|
const sceneState = sceneManager.getSceneState();
|
|
|
|
|
|
setStatus(locale === 'zh' ? `已保存场景: ${sceneState.sceneName}` : `Scene saved: ${sceneState.sceneName}`);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to save scene:', error);
|
|
|
|
|
|
setStatus(locale === 'zh' ? '保存场景失败' : 'Failed to save scene');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-10-17 18:13:31 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const handleSaveSceneAs = async () => {
|
|
|
|
|
|
if (!sceneManager) {
|
|
|
|
|
|
console.error('SceneManagerService not available');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-17 18:13:31 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
try {
|
|
|
|
|
|
await sceneManager.saveSceneAs();
|
|
|
|
|
|
const sceneState = sceneManager.getSceneState();
|
|
|
|
|
|
setStatus(locale === 'zh' ? `已保存场景: ${sceneState.sceneName}` : `Scene saved: ${sceneState.sceneName}`);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to save scene as:', error);
|
|
|
|
|
|
setStatus(locale === 'zh' ? '另存场景失败' : 'Failed to save scene as');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-10-17 18:13:31 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const handleCloseProject = async () => {
|
2025-11-27 20:42:46 +08:00
|
|
|
|
// 卸载项目插件
|
2025-11-02 23:50:41 +08:00
|
|
|
|
if (pluginManager) {
|
2025-11-18 14:46:51 +08:00
|
|
|
|
await pluginLoader.unloadProjectPlugins(pluginManager);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
}
|
2025-11-27 20:42:46 +08:00
|
|
|
|
|
|
|
|
|
|
// 清理模块系统
|
|
|
|
|
|
const engineService = EngineService.getInstance();
|
|
|
|
|
|
engineService.clearModuleSystems();
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭 ProjectService 中的项目
|
|
|
|
|
|
const projectService = Core.services.tryResolve(ProjectService);
|
|
|
|
|
|
if (projectService) {
|
|
|
|
|
|
await projectService.closeProject();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
setProjectLoaded(false);
|
|
|
|
|
|
setCurrentProjectPath(null);
|
|
|
|
|
|
setIsProfilerMode(false);
|
|
|
|
|
|
setStatus(t('header.status.ready'));
|
|
|
|
|
|
};
|
2025-10-15 18:24:13 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const handleExit = () => {
|
|
|
|
|
|
window.close();
|
|
|
|
|
|
};
|
2025-10-17 18:13:31 +08:00
|
|
|
|
|
2025-11-23 21:45:10 +08:00
|
|
|
|
const handleLocaleChange = (newLocale: Locale) => {
|
2025-11-02 23:50:41 +08:00
|
|
|
|
changeLocale(newLocale);
|
2025-10-17 18:13:31 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
// 通知所有已加载的插件更新语言
|
|
|
|
|
|
if (pluginManager) {
|
2025-11-27 20:42:46 +08:00
|
|
|
|
pluginManager.setLocale(newLocale);
|
2025-10-17 18:13:31 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
// 通过 MessageHub 通知需要重新获取节点模板
|
|
|
|
|
|
if (messageHub) {
|
|
|
|
|
|
messageHub.publish('locale:changed', { locale: newLocale });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-10-15 18:24:13 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const handleToggleDevtools = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await TauriAPI.toggleDevtools();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to toggle devtools:', error);
|
2025-10-28 17:19:28 +08:00
|
|
|
|
}
|
2025-11-02 23:50:41 +08:00
|
|
|
|
};
|
2025-10-28 17:19:28 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const handleOpenAbout = () => {
|
|
|
|
|
|
setShowAbout(true);
|
|
|
|
|
|
};
|
2025-10-15 09:34:44 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const handleCreatePlugin = () => {
|
|
|
|
|
|
setShowPluginGenerator(true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-18 14:46:51 +08:00
|
|
|
|
const handleReloadPlugins = async () => {
|
|
|
|
|
|
if (currentProjectPath && pluginManager) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1. 关闭所有动态面板
|
|
|
|
|
|
setActiveDynamicPanels([]);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 清空当前面板列表(强制卸载插件面板组件)
|
|
|
|
|
|
setPanels((prev) => prev.filter((p) =>
|
|
|
|
|
|
['scene-hierarchy', 'inspector', 'console', 'asset-browser'].includes(p.id)
|
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 等待React完成卸载
|
2025-11-23 14:49:37 +08:00
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
2025-11-18 14:46:51 +08:00
|
|
|
|
|
|
|
|
|
|
// 4. 卸载所有项目插件(清理UIRegistry、调用uninstall)
|
|
|
|
|
|
await pluginLoader.unloadProjectPlugins(pluginManager);
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 等待卸载完成
|
2025-11-23 14:49:37 +08:00
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
2025-11-18 14:46:51 +08:00
|
|
|
|
|
|
|
|
|
|
// 6. 重新加载插件
|
|
|
|
|
|
await pluginLoader.loadProjectPlugins(currentProjectPath, pluginManager);
|
|
|
|
|
|
|
|
|
|
|
|
// 7. 触发面板重新渲染
|
|
|
|
|
|
setPluginUpdateTrigger((prev) => prev + 1);
|
|
|
|
|
|
|
|
|
|
|
|
showToast(locale === 'zh' ? '插件已重新加载' : 'Plugins reloaded', 'success');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to reload plugins:', error);
|
|
|
|
|
|
showToast(locale === 'zh' ? '重新加载插件失败' : 'Failed to reload plugins', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (projectLoaded && entityStore && messageHub && logService && uiRegistry && pluginManager) {
|
|
|
|
|
|
let corePanels: FlexDockPanel[];
|
|
|
|
|
|
|
|
|
|
|
|
if (isProfilerMode) {
|
|
|
|
|
|
corePanels = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'scene-hierarchy',
|
|
|
|
|
|
title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy',
|
2025-11-23 21:45:10 +08:00
|
|
|
|
content: <SceneHierarchy entityStore={entityStore} messageHub={messageHub} commandManager={commandManager} isProfilerMode={true} />,
|
2025-11-02 23:50:41 +08:00
|
|
|
|
closable: false
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'inspector',
|
|
|
|
|
|
title: locale === 'zh' ? '检视器' : 'Inspector',
|
2025-11-21 10:03:18 +08:00
|
|
|
|
content: <Inspector entityStore={entityStore} messageHub={messageHub} inspectorRegistry={inspectorRegistry!} projectPath={currentProjectPath} commandManager={commandManager} />,
|
2025-11-02 23:50:41 +08:00
|
|
|
|
closable: false
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'console',
|
|
|
|
|
|
title: locale === 'zh' ? '控制台' : 'Console',
|
|
|
|
|
|
content: <ConsolePanel logService={logService} />,
|
|
|
|
|
|
closable: false
|
|
|
|
|
|
}
|
|
|
|
|
|
];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
corePanels = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'scene-hierarchy',
|
|
|
|
|
|
title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy',
|
2025-11-21 10:03:18 +08:00
|
|
|
|
content: <SceneHierarchy entityStore={entityStore} messageHub={messageHub} commandManager={commandManager} />,
|
|
|
|
|
|
closable: false
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'viewport',
|
|
|
|
|
|
title: locale === 'zh' ? '视口' : 'Viewport',
|
2025-11-23 14:49:37 +08:00
|
|
|
|
content: <Viewport locale={locale} messageHub={messageHub} />,
|
2025-11-02 23:50:41 +08:00
|
|
|
|
closable: false
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'inspector',
|
|
|
|
|
|
title: locale === 'zh' ? '检视器' : 'Inspector',
|
2025-11-21 10:03:18 +08:00
|
|
|
|
content: <Inspector entityStore={entityStore} messageHub={messageHub} inspectorRegistry={inspectorRegistry!} projectPath={currentProjectPath} commandManager={commandManager} />,
|
2025-11-02 23:50:41 +08:00
|
|
|
|
closable: false
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'assets',
|
|
|
|
|
|
title: locale === 'zh' ? '资产' : 'Assets',
|
2025-11-04 18:29:28 +08:00
|
|
|
|
content: <AssetBrowser projectPath={currentProjectPath} locale={locale} onOpenScene={handleOpenSceneByPath} />,
|
2025-11-02 23:50:41 +08:00
|
|
|
|
closable: false
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'console',
|
|
|
|
|
|
title: locale === 'zh' ? '控制台' : 'Console',
|
|
|
|
|
|
content: <ConsolePanel logService={logService} />,
|
|
|
|
|
|
closable: false
|
|
|
|
|
|
}
|
|
|
|
|
|
];
|
2025-10-15 23:24:13 +08:00
|
|
|
|
}
|
2025-11-02 23:50:41 +08:00
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
// 获取启用的插件面板
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const pluginPanels: FlexDockPanel[] = uiRegistry.getAllPanels()
|
|
|
|
|
|
.filter((panelDesc) => {
|
|
|
|
|
|
if (!panelDesc.component) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-11-04 18:29:28 +08:00
|
|
|
|
if (panelDesc.isDynamic) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-11-27 20:42:46 +08:00
|
|
|
|
return true;
|
2025-11-02 23:50:41 +08:00
|
|
|
|
})
|
|
|
|
|
|
.map((panelDesc) => {
|
|
|
|
|
|
const Component = panelDesc.component;
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: panelDesc.id,
|
|
|
|
|
|
title: (panelDesc as any).titleZh && locale === 'zh' ? (panelDesc as any).titleZh : panelDesc.title,
|
2025-11-18 14:46:51 +08:00
|
|
|
|
content: <Component key={`${panelDesc.id}-${pluginUpdateTrigger}`} projectPath={currentProjectPath} />,
|
2025-11-04 18:29:28 +08:00
|
|
|
|
closable: panelDesc.closable ?? true
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 添加激活的动态面板
|
|
|
|
|
|
const dynamicPanels: FlexDockPanel[] = activeDynamicPanels
|
2025-11-04 23:53:26 +08:00
|
|
|
|
.filter((panelId) => {
|
2025-11-04 18:29:28 +08:00
|
|
|
|
const panelDesc = uiRegistry.getPanel(panelId);
|
2025-11-28 10:32:28 +08:00
|
|
|
|
return panelDesc && (panelDesc.component || panelDesc.render);
|
2025-11-04 18:29:28 +08:00
|
|
|
|
})
|
2025-11-04 23:53:26 +08:00
|
|
|
|
.map((panelId) => {
|
2025-11-04 18:29:28 +08:00
|
|
|
|
const panelDesc = uiRegistry.getPanel(panelId)!;
|
2025-11-04 23:53:26 +08:00
|
|
|
|
// 优先使用动态标题,否则使用默认标题
|
|
|
|
|
|
const customTitle = dynamicPanelTitles.get(panelId);
|
|
|
|
|
|
const defaultTitle = (panelDesc as any).titleZh && locale === 'zh' ? (panelDesc as any).titleZh : panelDesc.title;
|
2025-11-28 10:32:28 +08:00
|
|
|
|
|
|
|
|
|
|
// 支持 component 或 render 两种方式
|
|
|
|
|
|
let content: React.ReactNode;
|
|
|
|
|
|
if (panelDesc.component) {
|
|
|
|
|
|
const Component = panelDesc.component;
|
|
|
|
|
|
content = <Component projectPath={currentProjectPath} />;
|
|
|
|
|
|
} else if (panelDesc.render) {
|
|
|
|
|
|
content = panelDesc.render();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-04 18:29:28 +08:00
|
|
|
|
return {
|
|
|
|
|
|
id: panelDesc.id,
|
2025-11-04 23:53:26 +08:00
|
|
|
|
title: customTitle || defaultTitle,
|
2025-11-28 10:32:28 +08:00
|
|
|
|
content,
|
2025-11-02 23:50:41 +08:00
|
|
|
|
closable: panelDesc.closable ?? true
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-04 18:29:28 +08:00
|
|
|
|
setPanels([...corePanels, ...pluginPanels, ...dynamicPanels]);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
}
|
2025-11-04 23:53:26 +08:00
|
|
|
|
}, [projectLoaded, entityStore, messageHub, logService, uiRegistry, pluginManager, locale, currentProjectPath, t, pluginUpdateTrigger, isProfilerMode, handleOpenSceneByPath, activeDynamicPanels, dynamicPanelTitles]);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!initialized) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="editor-loading">
|
|
|
|
|
|
<Loader2 size={32} className="animate-spin" />
|
|
|
|
|
|
<h2>Loading Editor...</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
2025-10-15 09:58:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
if (!projectLoaded) {
|
|
|
|
|
|
const settings = SettingsService.getInstance();
|
|
|
|
|
|
const recentProjects = settings.getRecentProjects();
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<StartupPage
|
|
|
|
|
|
onOpenProject={handleOpenProject}
|
|
|
|
|
|
onCreateProject={handleCreateProject}
|
|
|
|
|
|
onOpenRecentProject={handleOpenRecentProject}
|
|
|
|
|
|
onProfilerMode={handleProfilerMode}
|
2025-11-23 21:45:10 +08:00
|
|
|
|
onLocaleChange={handleLocaleChange}
|
2025-11-02 23:50:41 +08:00
|
|
|
|
recentProjects={recentProjects}
|
|
|
|
|
|
locale={locale}
|
|
|
|
|
|
/>
|
2025-11-23 21:45:10 +08:00
|
|
|
|
<ProjectCreationWizard
|
|
|
|
|
|
isOpen={showProjectWizard}
|
|
|
|
|
|
onClose={() => setShowProjectWizard(false)}
|
|
|
|
|
|
onCreateProject={handleCreateProjectFromWizard}
|
|
|
|
|
|
onBrowsePath={handleBrowseProjectPath}
|
|
|
|
|
|
locale={locale}
|
|
|
|
|
|
/>
|
2025-11-02 23:50:41 +08:00
|
|
|
|
{isLoading && (
|
|
|
|
|
|
<div className="loading-overlay">
|
|
|
|
|
|
<div className="loading-content">
|
|
|
|
|
|
<Loader2 size={40} className="animate-spin" />
|
|
|
|
|
|
<p className="loading-message">{loadingMessage}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{errorDialog && (
|
|
|
|
|
|
<ErrorDialog
|
|
|
|
|
|
title={errorDialog.title}
|
|
|
|
|
|
message={errorDialog.message}
|
|
|
|
|
|
onClose={() => setErrorDialog(null)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{confirmDialog && (
|
|
|
|
|
|
<ConfirmDialog
|
|
|
|
|
|
title={confirmDialog.title}
|
|
|
|
|
|
message={confirmDialog.message}
|
|
|
|
|
|
confirmText={confirmDialog.confirmText}
|
|
|
|
|
|
cancelText={confirmDialog.cancelText}
|
2025-11-18 14:46:51 +08:00
|
|
|
|
onConfirm={() => {
|
|
|
|
|
|
confirmDialog.onConfirm();
|
|
|
|
|
|
setConfirmDialog(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
onCancel={() => {
|
|
|
|
|
|
if (confirmDialog.onCancel) {
|
|
|
|
|
|
confirmDialog.onCancel();
|
|
|
|
|
|
}
|
|
|
|
|
|
setConfirmDialog(null);
|
|
|
|
|
|
}}
|
2025-11-02 23:50:41 +08:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-10-15 17:15:05 +08:00
|
|
|
|
|
2025-11-23 21:45:10 +08:00
|
|
|
|
const projectName = currentProjectPath ? currentProjectPath.split(/[\\/]/).pop() : 'Untitled';
|
|
|
|
|
|
|
2025-10-15 09:34:44 +08:00
|
|
|
|
return (
|
2025-11-02 23:50:41 +08:00
|
|
|
|
<div className="editor-container">
|
2025-11-04 23:53:26 +08:00
|
|
|
|
{!isEditorFullscreen && (
|
2025-11-23 21:45:10 +08:00
|
|
|
|
<>
|
|
|
|
|
|
<div className="editor-titlebar" data-tauri-drag-region>
|
|
|
|
|
|
<span className="titlebar-project-name">{projectName}</span>
|
2025-11-27 20:42:46 +08:00
|
|
|
|
<span className="titlebar-app-name">ESEngine Editor</span>
|
2025-11-23 21:45:10 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div className={`editor-header ${isRemoteConnected ? 'remote-connected' : ''}`}>
|
|
|
|
|
|
<MenuBar
|
2025-11-04 23:53:26 +08:00
|
|
|
|
locale={locale}
|
|
|
|
|
|
uiRegistry={uiRegistry || undefined}
|
|
|
|
|
|
messageHub={messageHub || undefined}
|
|
|
|
|
|
pluginManager={pluginManager || undefined}
|
|
|
|
|
|
onNewScene={handleNewScene}
|
|
|
|
|
|
onOpenScene={handleOpenScene}
|
|
|
|
|
|
onSaveScene={handleSaveScene}
|
|
|
|
|
|
onSaveSceneAs={handleSaveSceneAs}
|
|
|
|
|
|
onOpenProject={handleOpenProject}
|
|
|
|
|
|
onCloseProject={handleCloseProject}
|
|
|
|
|
|
onExit={handleExit}
|
2025-11-27 20:42:46 +08:00
|
|
|
|
onOpenPluginManager={() => {
|
|
|
|
|
|
setSettingsInitialCategory('plugins');
|
|
|
|
|
|
setShowSettings(true);
|
|
|
|
|
|
}}
|
2025-11-04 23:53:26 +08:00
|
|
|
|
onOpenProfiler={() => setShowProfiler(true)}
|
|
|
|
|
|
onOpenPortManager={() => setShowPortManager(true)}
|
|
|
|
|
|
onOpenSettings={() => setShowSettings(true)}
|
|
|
|
|
|
onToggleDevtools={handleToggleDevtools}
|
|
|
|
|
|
onOpenAbout={handleOpenAbout}
|
|
|
|
|
|
onCreatePlugin={handleCreatePlugin}
|
2025-11-18 14:46:51 +08:00
|
|
|
|
onReloadPlugins={handleReloadPlugins}
|
2025-11-04 23:53:26 +08:00
|
|
|
|
/>
|
|
|
|
|
|
<div className="header-right">
|
2025-11-23 21:45:10 +08:00
|
|
|
|
<div className="locale-dropdown" ref={localeMenuRef}>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="toolbar-btn locale-btn"
|
|
|
|
|
|
onClick={() => setShowLocaleMenu(!showLocaleMemu)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Globe size={14} />
|
|
|
|
|
|
<span className="locale-label">{locale === 'en' ? 'EN' : '中'}</span>
|
|
|
|
|
|
<ChevronDown size={10} />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{showLocaleMemu && (
|
|
|
|
|
|
<div className="locale-menu">
|
|
|
|
|
|
<button
|
|
|
|
|
|
className={`locale-menu-item ${locale === 'en' ? 'active' : ''}`}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
handleLocaleChange('en');
|
|
|
|
|
|
setShowLocaleMenu(false);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
English
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className={`locale-menu-item ${locale === 'zh' ? 'active' : ''}`}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
handleLocaleChange('zh');
|
|
|
|
|
|
setShowLocaleMenu(false);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
中文
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-11-04 23:53:26 +08:00
|
|
|
|
<span className="status">{status}</span>
|
|
|
|
|
|
</div>
|
2025-11-23 21:45:10 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
2025-11-04 23:53:26 +08:00
|
|
|
|
)}
|
2025-10-15 09:34:44 +08:00
|
|
|
|
|
2025-11-18 14:46:51 +08:00
|
|
|
|
<CompilerConfigDialog
|
|
|
|
|
|
isOpen={compilerDialog.isOpen}
|
|
|
|
|
|
compilerId={compilerDialog.compilerId}
|
|
|
|
|
|
projectPath={currentProjectPath}
|
|
|
|
|
|
currentFileName={compilerDialog.currentFileName}
|
|
|
|
|
|
onClose={() => setCompilerDialog({ isOpen: false, compilerId: '' })}
|
|
|
|
|
|
onCompileComplete={(result) => {
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
|
showToast(result.message, 'success');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showToast(result.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
<div className="editor-content">
|
2025-11-04 18:29:28 +08:00
|
|
|
|
<FlexLayoutDockContainer
|
|
|
|
|
|
panels={panels}
|
2025-11-04 23:53:26 +08:00
|
|
|
|
activePanelId={activePanelId}
|
2025-11-04 18:29:28 +08:00
|
|
|
|
onPanelClose={(panelId) => {
|
2025-11-04 23:53:26 +08:00
|
|
|
|
logger.info('Panel closed:', panelId);
|
|
|
|
|
|
setActiveDynamicPanels((prev) => prev.filter((id) => id !== panelId));
|
2025-11-04 18:29:28 +08:00
|
|
|
|
}}
|
|
|
|
|
|
/>
|
2025-11-02 23:50:41 +08:00
|
|
|
|
</div>
|
2025-10-16 17:10:22 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
<div className="editor-footer">
|
2025-11-27 20:42:46 +08:00
|
|
|
|
<span>{t('footer.plugins')}: {pluginManager?.getAllPlugins().length ?? 0}</span>
|
2025-11-02 23:50:41 +08:00
|
|
|
|
<span>{t('footer.entities')}: {entityStore?.getAllEntities().length ?? 0}</span>
|
|
|
|
|
|
<span>{t('footer.core')}: {t('footer.active')}</span>
|
2025-10-15 18:29:48 +08:00
|
|
|
|
</div>
|
2025-11-02 23:50:41 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{showProfiler && (
|
|
|
|
|
|
<ProfilerWindow onClose={() => setShowProfiler(false)} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{showPortManager && (
|
|
|
|
|
|
<PortManager onClose={() => setShowPortManager(false)} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{showSettings && settingsRegistry && (
|
2025-11-27 20:42:46 +08:00
|
|
|
|
<SettingsWindow
|
|
|
|
|
|
onClose={() => {
|
|
|
|
|
|
setShowSettings(false);
|
|
|
|
|
|
setSettingsInitialCategory(undefined);
|
|
|
|
|
|
}}
|
|
|
|
|
|
settingsRegistry={settingsRegistry}
|
|
|
|
|
|
initialCategoryId={settingsInitialCategory}
|
|
|
|
|
|
/>
|
2025-11-02 23:50:41 +08:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{showAbout && (
|
|
|
|
|
|
<AboutDialog onClose={() => setShowAbout(false)} locale={locale} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{showPluginGenerator && (
|
|
|
|
|
|
<PluginGeneratorWindow
|
|
|
|
|
|
onClose={() => setShowPluginGenerator(false)}
|
|
|
|
|
|
projectPath={currentProjectPath}
|
|
|
|
|
|
locale={locale}
|
|
|
|
|
|
onSuccess={async () => {
|
|
|
|
|
|
if (currentProjectPath && pluginManager) {
|
2025-11-18 14:46:51 +08:00
|
|
|
|
await pluginLoader.loadProjectPlugins(currentProjectPath, pluginManager);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{errorDialog && (
|
|
|
|
|
|
<ErrorDialog
|
|
|
|
|
|
title={errorDialog.title}
|
|
|
|
|
|
message={errorDialog.message}
|
|
|
|
|
|
onClose={() => setErrorDialog(null)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2025-11-18 14:46:51 +08:00
|
|
|
|
|
|
|
|
|
|
{confirmDialog && (
|
|
|
|
|
|
<ConfirmDialog
|
|
|
|
|
|
title={confirmDialog.title}
|
|
|
|
|
|
message={confirmDialog.message}
|
|
|
|
|
|
confirmText={confirmDialog.confirmText}
|
|
|
|
|
|
cancelText={confirmDialog.cancelText}
|
|
|
|
|
|
onConfirm={() => {
|
|
|
|
|
|
confirmDialog.onConfirm();
|
|
|
|
|
|
setConfirmDialog(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
onCancel={() => {
|
|
|
|
|
|
if (confirmDialog.onCancel) {
|
|
|
|
|
|
confirmDialog.onCancel();
|
|
|
|
|
|
}
|
|
|
|
|
|
setConfirmDialog(null);
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2025-10-14 23:31:09 +08:00
|
|
|
|
</div>
|
2025-11-02 23:50:41 +08:00
|
|
|
|
);
|
2025-10-14 22:53:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-27 09:29:11 +08:00
|
|
|
|
function AppWithToast() {
|
2025-11-02 23:50:41 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<ToastProvider>
|
|
|
|
|
|
<App />
|
|
|
|
|
|
</ToastProvider>
|
|
|
|
|
|
);
|
2025-10-27 09:29:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default AppWithToast;
|