From cadf147b740804fb5024be27873d3c83ce27f39e Mon Sep 17 00:00:00 2001 From: yhh <359807859@qq.com> Date: Thu, 4 Dec 2025 11:20:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=99=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src-tauri/src/commands/system.rs | 27 ++- packages/editor-app/src/App.tsx | 26 ++- .../src/components/ContentBrowser.tsx | 3 +- .../editor-app/src/components/FileTree.tsx | 3 +- .../editor-app/src/components/StartupPage.tsx | 112 ++++++++++- .../src/services/SettingsService.ts | 6 +- .../editor-app/src/styles/StartupPage.css | 180 ++++++++++++++++++ .../src/Services/ProjectService.ts | 27 ++- 8 files changed, 371 insertions(+), 13 deletions(-) diff --git a/packages/editor-app/src-tauri/src/commands/system.rs b/packages/editor-app/src-tauri/src/commands/system.rs index 309964b2..62d64e7e 100644 --- a/packages/editor-app/src-tauri/src/commands/system.rs +++ b/packages/editor-app/src-tauri/src/commands/system.rs @@ -75,10 +75,35 @@ pub fn open_file_with_default_app(file_path: String) -> Result<(), String> { /// Show file in system file explorer #[tauri::command] pub fn show_in_folder(file_path: String) -> Result<(), String> { + println!("[show_in_folder] Received path: {}", file_path); + #[cfg(target_os = "windows")] { + use std::path::Path; + + // Normalize path separators for Windows + // 规范化路径分隔符 + let normalized_path = file_path.replace('/', "\\"); + println!("[show_in_folder] Normalized path: {}", normalized_path); + + // Verify the path exists before trying to show it + // 验证路径存在 + let path = Path::new(&normalized_path); + let exists = path.exists(); + println!("[show_in_folder] Path exists: {}", exists); + + if !exists { + return Err(format!("Path does not exist: {}", normalized_path)); + } + + // Windows explorer requires /select, to be concatenated with the path + // without spaces. Use a single argument to avoid shell parsing issues. + // Windows 资源管理器要求 /select, 与路径连接在一起,中间没有空格 + let select_arg = format!("/select,{}", normalized_path); + println!("[show_in_folder] Explorer arg: {}", select_arg); + Command::new("explorer") - .args(["/select,", &file_path]) + .arg(&select_arg) .spawn() .map_err(|e| format!("Failed to show in folder: {}", e))?; } diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index b1dfc369..db306694 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -465,7 +465,9 @@ function App() { }; const handleCreateProjectFromWizard = async (projectName: string, projectPath: string, _templateId: string) => { - const fullProjectPath = `${projectPath}\\${projectName}`; + // 使用与 projectPath 相同的路径分隔符 | Use same separator as projectPath + const sep = projectPath.includes('/') ? '/' : '\\'; + const fullProjectPath = `${projectPath}${sep}${projectName}`; try { setIsLoading(true); @@ -824,6 +826,28 @@ function App() { onOpenProject={handleOpenProject} onCreateProject={handleCreateProject} onOpenRecentProject={handleOpenRecentProject} + onRemoveRecentProject={(projectPath) => { + settings.removeRecentProject(projectPath); + // 强制重新渲染 | Force re-render + setStatus(t('header.status.ready')); + }} + onDeleteProject={async (projectPath) => { + try { + await TauriAPI.deleteFolder(projectPath); + // 删除成功后从列表中移除并触发重新渲染 + // Remove from list and trigger re-render after successful deletion + settings.removeRecentProject(projectPath); + setStatus(t('header.status.ready')); + } catch (error) { + console.error('Failed to delete project:', error); + setErrorDialog({ + title: locale === 'zh' ? '删除项目失败' : 'Failed to Delete Project', + message: locale === 'zh' + ? `无法删除项目:\n${error instanceof Error ? error.message : String(error)}` + : `Failed to delete project:\n${error instanceof Error ? error.message : String(error)}` + }); + } + }} onLocaleChange={handleLocaleChange} recentProjects={recentProjects} locale={locale} diff --git a/packages/editor-app/src/components/ContentBrowser.tsx b/packages/editor-app/src/components/ContentBrowser.tsx index 0fb3d6ce..57e3307d 100644 --- a/packages/editor-app/src/components/ContentBrowser.tsx +++ b/packages/editor-app/src/components/ContentBrowser.tsx @@ -799,9 +799,10 @@ export function ContentBrowser({ icon: , onClick: async () => { try { + console.log('[ContentBrowser] showInFolder path:', asset.path); await TauriAPI.showInFolder(asset.path); } catch (error) { - console.error('Failed to show in folder:', error); + console.error('Failed to show in folder:', error, 'Path:', asset.path); } } }); diff --git a/packages/editor-app/src/components/FileTree.tsx b/packages/editor-app/src/components/FileTree.tsx index 74c589fd..be3a2c44 100644 --- a/packages/editor-app/src/components/FileTree.tsx +++ b/packages/editor-app/src/components/FileTree.tsx @@ -833,9 +833,10 @@ export const FileTree = forwardRef(({ rootPath, o icon: , onClick: async () => { try { + console.log('[FileTree] showInFolder path:', node.path); await TauriAPI.showInFolder(node.path); } catch (error) { - console.error('Failed to show in folder:', error); + console.error('Failed to show in folder:', error, 'Path:', node.path); } } }); diff --git a/packages/editor-app/src/components/StartupPage.tsx b/packages/editor-app/src/components/StartupPage.tsx index f39e1541..95700d96 100644 --- a/packages/editor-app/src/components/StartupPage.tsx +++ b/packages/editor-app/src/components/StartupPage.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from 'react'; import { getVersion } from '@tauri-apps/api/app'; -import { Globe, ChevronDown, Download, X, Loader2 } from 'lucide-react'; +import { Globe, ChevronDown, Download, X, Loader2, Trash2 } from 'lucide-react'; import { checkForUpdatesOnStartup, installUpdate, type UpdateCheckResult } from '../utils/updater'; import { StartupLogo } from './StartupLogo'; import '../styles/StartupPage.css'; @@ -11,6 +11,8 @@ interface StartupPageProps { onOpenProject: () => void; onCreateProject: () => void; onOpenRecentProject?: (projectPath: string) => void; + onRemoveRecentProject?: (projectPath: string) => void; + onDeleteProject?: (projectPath: string) => Promise; onLocaleChange?: (locale: Locale) => void; recentProjects?: string[]; locale: string; @@ -21,11 +23,13 @@ const LANGUAGES = [ { code: 'zh', name: '中文' } ]; -export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onLocaleChange, recentProjects = [], locale }: StartupPageProps) { +export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onRemoveRecentProject, onDeleteProject, onLocaleChange, recentProjects = [], locale }: StartupPageProps) { const [showLogo, setShowLogo] = useState(true); const [hoveredProject, setHoveredProject] = useState(null); const [appVersion, setAppVersion] = useState(''); const [showLangMenu, setShowLangMenu] = useState(false); + const [contextMenu, setContextMenu] = useState<{ x: number; y: number; project: string } | null>(null); + const [deleteConfirm, setDeleteConfirm] = useState(null); const [updateInfo, setUpdateInfo] = useState(null); const [showUpdateBanner, setShowUpdateBanner] = useState(false); const [isInstalling, setIsInstalling] = useState(false); @@ -66,7 +70,13 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec updateAvailable: 'New version available', updateNow: 'Update Now', installing: 'Installing...', - later: 'Later' + later: 'Later', + removeFromList: 'Remove from List', + deleteProject: 'Delete Project', + deleteConfirmTitle: 'Delete Project', + deleteConfirmMessage: 'Are you sure you want to permanently delete this project? This action cannot be undone.', + cancel: 'Cancel', + delete: 'Delete' }, zh: { title: 'ESEngine 编辑器', @@ -78,7 +88,13 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec updateAvailable: '发现新版本', updateNow: '立即更新', installing: '正在安装...', - later: '稍后' + later: '稍后', + removeFromList: '从列表中移除', + deleteProject: '删除项目', + deleteConfirmTitle: '删除项目', + deleteConfirmMessage: '确定要永久删除此项目吗?此操作无法撤销。', + cancel: '取消', + delete: '删除' } }; @@ -136,6 +152,10 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec onMouseEnter={() => setHoveredProject(project)} onMouseLeave={() => setHoveredProject(null)} onClick={() => onOpenRecentProject?.(project)} + onContextMenu={(e) => { + e.preventDefault(); + setContextMenu({ x: e.clientX, y: e.clientY, project }); + }} style={{ cursor: onOpenRecentProject ? 'pointer' : 'default' }} > @@ -145,6 +165,18 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec {project.split(/[\\/]/).pop()} {project} + {onRemoveRecentProject && ( + { + e.stopPropagation(); + onRemoveRecentProject(project); + }} + title={t.removeFromList} + > + + + )} ))} @@ -217,6 +249,78 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec )} + + {/* 右键菜单 | Context Menu */} + {contextMenu && ( + setContextMenu(null)} + > + e.stopPropagation()} + > + { + onRemoveRecentProject?.(contextMenu.project); + setContextMenu(null); + }} + > + + {t.removeFromList} + + {onDeleteProject && ( + { + setDeleteConfirm(contextMenu.project); + setContextMenu(null); + }} + > + + {t.deleteProject} + + )} + + + )} + + {/* 删除确认对话框 | Delete Confirmation Dialog */} + {deleteConfirm && ( + + + + + {t.deleteConfirmTitle} + + + {t.deleteConfirmMessage} + {deleteConfirm} + + + setDeleteConfirm(null)} + > + {t.cancel} + + { + if (deleteConfirm && onDeleteProject) { + await onDeleteProject(deleteConfirm); + } + setDeleteConfirm(null); + }} + > + {t.delete} + + + + + )} ); } diff --git a/packages/editor-app/src/services/SettingsService.ts b/packages/editor-app/src/services/SettingsService.ts index 5b9105c0..c4bba05b 100644 --- a/packages/editor-app/src/services/SettingsService.ts +++ b/packages/editor-app/src/services/SettingsService.ts @@ -70,9 +70,11 @@ export class SettingsService { } public addRecentProject(projectPath: string): void { + // 规范化路径,防止双重转义 | Normalize path to prevent double escaping + const normalizedPath = projectPath.replace(/\\\\/g, '\\'); const recentProjects = this.getRecentProjects(); - const filtered = recentProjects.filter((p) => p !== projectPath); - const updated = [projectPath, ...filtered].slice(0, 10); + const filtered = recentProjects.filter((p) => p !== normalizedPath); + const updated = [normalizedPath, ...filtered].slice(0, 10); this.set('recentProjects', updated); } diff --git a/packages/editor-app/src/styles/StartupPage.css b/packages/editor-app/src/styles/StartupPage.css index 2d10a33a..fd50aaac 100644 --- a/packages/editor-app/src/styles/StartupPage.css +++ b/packages/editor-app/src/styles/StartupPage.css @@ -174,6 +174,32 @@ white-space: nowrap; } +.recent-remove-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + background: transparent; + border: none; + border-radius: 4px; + color: #6e6e6e; + cursor: pointer; + opacity: 0; + transition: all 0.15s; + flex-shrink: 0; +} + +.recent-item:hover .recent-remove-btn { + opacity: 1; +} + +.recent-remove-btn:hover { + background: rgba(255, 80, 80, 0.15); + color: #f87171; +} + .startup-footer { display: flex; align-items: center; @@ -346,3 +372,157 @@ opacity: 0.5; cursor: not-allowed; } + +/* 右键菜单样式 | Context Menu Styles */ +.startup-context-menu-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; +} + +.startup-context-menu { + position: fixed; + min-width: 180px; + background: #252529; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + padding: 4px 0; + z-index: 1001; +} + +.startup-context-menu-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 8px 14px; + background: transparent; + border: none; + color: #cccccc; + font-size: 13px; + text-align: left; + cursor: pointer; + transition: all 0.1s; +} + +.startup-context-menu-item:hover { + background: #3b82f6; + color: #ffffff; +} + +.startup-context-menu-item.danger { + color: #f87171; +} + +.startup-context-menu-item.danger:hover { + background: #dc2626; + color: #ffffff; +} + +/* 对话框样式 | Dialog Styles */ +.startup-dialog-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 1002; +} + +.startup-dialog { + width: 400px; + background: #2d2d30; + border: 1px solid #3e3e42; + border-radius: 8px; + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5); + overflow: hidden; +} + +.startup-dialog-header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + background: #252526; + border-bottom: 1px solid #3e3e42; +} + +.startup-dialog-header h3 { + margin: 0; + font-size: 15px; + font-weight: 500; + color: #ffffff; +} + +.dialog-icon-danger { + color: #f87171; +} + +.startup-dialog-body { + padding: 20px; +} + +.startup-dialog-body p { + margin: 0 0 12px 0; + font-size: 13px; + color: #cccccc; + line-height: 1.5; +} + +.startup-dialog-body p:last-child { + margin-bottom: 0; +} + +.startup-dialog-path { + padding: 10px 12px; + background: #1e1e1e; + border-radius: 4px; + font-family: monospace; + font-size: 12px; + color: #858585; + word-break: break-all; +} + +.startup-dialog-footer { + display: flex; + justify-content: flex-end; + gap: 10px; + padding: 16px 20px; + background: #252526; + border-top: 1px solid #3e3e42; +} + +.startup-dialog-btn { + padding: 8px 16px; + border: 1px solid #3e3e42; + border-radius: 4px; + background: #2d2d30; + color: #cccccc; + font-size: 13px; + cursor: pointer; + transition: all 0.15s; +} + +.startup-dialog-btn:hover { + background: #37373d; + border-color: #555; +} + +.startup-dialog-btn.danger { + background: #dc2626; + border-color: #dc2626; + color: #ffffff; +} + +.startup-dialog-btn.danger:hover { + background: #b91c1c; + border-color: #b91c1c; +} diff --git a/packages/editor-core/src/Services/ProjectService.ts b/packages/editor-core/src/Services/ProjectService.ts index c264a22e..b585a604 100644 --- a/packages/editor-core/src/Services/ProjectService.ts +++ b/packages/editor-core/src/Services/ProjectService.ts @@ -94,11 +94,15 @@ export class ProjectService implements IService { scriptsPath: 'scripts', buildOutput: '.esengine/compiled', scenesPath: 'scenes', - defaultScene: 'main.ecs' + defaultScene: 'main.ecs', + plugins: { enabledPlugins: [] }, + modules: { disabledModules: [] } }; await this.fileAPI.writeFileContent(configPath, JSON.stringify(config, null, 2)); + // Create scenes folder and default scene + // 创建场景文件夹和默认场景 const scenesPath = `${projectPath}${sep}${config.scenesPath}`; await this.fileAPI.createDirectory(scenesPath); @@ -111,6 +115,21 @@ export class ProjectService implements IService { }) as string; await this.fileAPI.writeFileContent(defaultScenePath, sceneData); + // Create scripts folder for user scripts + // 创建用户脚本文件夹 + const scriptsPath = `${projectPath}${sep}${config.scriptsPath}`; + await this.fileAPI.createDirectory(scriptsPath); + + // Create scripts/editor folder for editor extension scripts + // 创建编辑器扩展脚本文件夹 + const editorScriptsPath = `${scriptsPath}${sep}editor`; + await this.fileAPI.createDirectory(editorScriptsPath); + + // Create assets folder for project assets (textures, audio, etc.) + // 创建资源文件夹(纹理、音频等) + const assetsPath = `${projectPath}${sep}assets`; + await this.fileAPI.createDirectory(assetsPath); + await this.messageHub.publish('project:created', { path: projectPath }); @@ -258,8 +277,10 @@ export class ProjectService implements IService { scenesPath: config.scenesPath || 'scenes', defaultScene: config.defaultScene || 'main.ecs', uiDesignResolution: config.uiDesignResolution, - plugins: config.plugins, - modules: config.modules + // Provide default empty plugins config for legacy projects + // 为旧项目提供默认的空插件配置 + plugins: config.plugins || { enabledPlugins: [] }, + modules: config.modules || { disabledModules: [] } }; logger.debug('Loaded config result:', result); return result;
{t.deleteConfirmMessage}
{deleteConfirm}