fix: 修复项目切换时运行时和系统重复初始化问题 (#267)

* feat(editor): 添加 GitHub Discussions 社区论坛功能

* chore: 更新 pnpm-lock.yaml

* chore: 删除测试图片

* refactor: 改用 Imgur 图床上传图片

* fix: 修复项目切换时运行时和系统重复初始化问题

* fix: 修复多个编辑器问题

* feat: 添加脚本编辑器配置和类型定义支持

* feat: 实现资产 .meta 文件自动生成功能
This commit is contained in:
YHH
2025-12-04 14:04:39 +08:00
committed by GitHub
parent 374b26f7c6
commit b4e7ba2abd
20 changed files with 1040 additions and 75 deletions

View File

@@ -40,6 +40,7 @@ import {
import { Core } from '@esengine/ecs-framework';
import { MessageHub, FileActionRegistry, type FileCreationTemplate } from '@esengine/editor-core';
import { TauriAPI, DirectoryEntry } from '../api/tauri';
import { SettingsService } from '../services/SettingsService';
import { ContextMenu, ContextMenuItem } from './ContextMenu';
import { PromptDialog } from './PromptDialog';
import '../styles/ContentBrowser.css';
@@ -210,8 +211,124 @@ export function ContentBrowser({
'Shader': { en: 'Shader', zh: '着色器' },
'Tilemap': { en: 'Tilemap', zh: '瓦片地图' },
'Tileset': { en: 'Tileset', zh: '瓦片集' },
'Component': { en: 'Component', zh: '组件' },
'System': { en: 'System', zh: '系统' },
'TypeScript': { en: 'TypeScript', zh: 'TypeScript' },
};
// 注册内置的 TypeScript 文件创建模板
// Register built-in TypeScript file creation templates
useEffect(() => {
if (!fileActionRegistry) return;
const builtinTemplates: FileCreationTemplate[] = [
{
id: 'ts-component',
label: 'Component',
extension: '.ts',
icon: 'FileCode',
category: 'Script',
getContent: (fileName: string) => {
const className = fileName.replace(/\.ts$/, '');
return `import { Component, ECSComponent, Property, Serialize, Serializable } from '@esengine/ecs-framework';
/**
* ${className}
*/
@ECSComponent('${className}')
@Serializable({ version: 1, typeId: '${className}' })
export class ${className} extends Component {
// 在这里添加组件属性
// Add component properties here
@Serialize()
@Property({ type: 'number', label: 'Example Property' })
public exampleProperty: number = 0;
onInitialize(): void {
// 组件初始化时调用
// Called when component is initialized
}
onDestroy(): void {
// 组件销毁时调用
// Called when component is destroyed
}
}
`;
}
},
{
id: 'ts-system',
label: 'System',
extension: '.ts',
icon: 'FileCode',
category: 'Script',
getContent: (fileName: string) => {
const className = fileName.replace(/\.ts$/, '');
return `import { EntitySystem, Matcher, type Entity } from '@esengine/ecs-framework';
/**
* ${className}
*/
export class ${className} extends EntitySystem {
// 定义系统处理的组件类型
// Define component types this system processes
protected getMatcher(): Matcher {
// 返回匹配器,指定需要哪些组件
// Return matcher specifying required components
// return Matcher.all(SomeComponent);
return Matcher.empty();
}
protected updateEntity(entity: Entity, deltaTime: number): void {
// 处理每个实体
// Process each entity
}
// 可选:系统初始化
// Optional: System initialization
// onInitialize(): void {
// super.onInitialize();
// }
}
`;
}
},
{
id: 'ts-script',
label: 'TypeScript',
extension: '.ts',
icon: 'FileCode',
category: 'Script',
getContent: (fileName: string) => {
const name = fileName.replace(/\.ts$/, '');
return `/**
* ${name}
*/
export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
// 在这里编写代码
// Write your code here
}
`;
}
}
];
// 注册模板
for (const template of builtinTemplates) {
fileActionRegistry.registerCreationTemplate(template);
}
// 清理函数
return () => {
for (const template of builtinTemplates) {
fileActionRegistry.unregisterCreationTemplate(template);
}
};
}, [fileActionRegistry]);
const getTemplateLabel = (label: string): string => {
const mapping = templateLabels[label];
if (mapping) {
@@ -439,6 +556,24 @@ export function ContentBrowser({
return;
}
// 脚本文件使用配置的编辑器打开
// Open script files with configured editor
if (ext === 'ts' || ext === 'tsx' || ext === 'js' || ext === 'jsx') {
const settings = SettingsService.getInstance();
const editorCommand = settings.getScriptEditorCommand();
if (editorCommand && projectPath) {
try {
await TauriAPI.openWithEditor(projectPath, editorCommand, asset.path);
return;
} catch (error) {
console.error('Failed to open with editor:', error);
// 如果失败,回退到系统默认应用
// Fall back to system default app if failed
}
}
}
if (fileActionRegistry) {
const handled = await fileActionRegistry.handleDoubleClick(asset.path);
if (handled) return;
@@ -450,7 +585,7 @@ export function ContentBrowser({
console.error('Failed to open file:', error);
}
}
}, [loadAssets, onOpenScene, fileActionRegistry]);
}, [loadAssets, onOpenScene, fileActionRegistry, projectPath]);
// Handle context menu
const handleContextMenu = useCallback((e: React.MouseEvent, asset?: AssetItem) => {
@@ -799,9 +934,10 @@ export function ContentBrowser({
icon: <ExternalLink size={16} />,
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);
}
}
});
@@ -1126,10 +1262,16 @@ export function ContentBrowser({
)}
{/* Create File Dialog */}
{createFileDialog && (
{createFileDialog && (() => {
// 规范化扩展名(确保有点号前缀)
// Normalize extension (ensure dot prefix)
const ext = createFileDialog.template.extension.startsWith('.')
? createFileDialog.template.extension
: `.${createFileDialog.template.extension}`;
return (
<PromptDialog
title={`New ${createFileDialog.template.label}`}
message={`Enter file name (.${createFileDialog.template.extension} will be added):`}
title={locale === 'zh' ? `新建 ${getTemplateLabel(createFileDialog.template.label)}` : `New ${createFileDialog.template.label}`}
message={locale === 'zh' ? `输入文件名(将添加 ${ext}:` : `Enter file name (${ext} will be added):`}
placeholder="filename"
confirmText={locale === 'zh' ? '创建' : 'Create'}
cancelText={locale === 'zh' ? '取消' : 'Cancel'}
@@ -1138,8 +1280,8 @@ export function ContentBrowser({
setCreateFileDialog(null);
let fileName = value;
if (!fileName.endsWith(`.${template.extension}`)) {
fileName = `${fileName}.${template.extension}`;
if (!fileName.endsWith(ext)) {
fileName = `${fileName}${ext}`;
}
const filePath = `${parentPath}/${fileName}`;
@@ -1155,7 +1297,8 @@ export function ContentBrowser({
}}
onCancel={() => setCreateFileDialog(null)}
/>
)}
);
})()}
</div>
);
}

View File

@@ -833,9 +833,10 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
icon: <FolderOpen size={16} />,
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);
}
}
});

View File

@@ -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<void>;
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<string | null>(null);
const [appVersion, setAppVersion] = useState<string>('');
const [showLangMenu, setShowLangMenu] = useState(false);
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; project: string } | null>(null);
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
const [updateInfo, setUpdateInfo] = useState<UpdateCheckResult | null>(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' }}
>
<svg className="recent-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
@@ -145,6 +165,18 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
<div className="recent-name">{project.split(/[\\/]/).pop()}</div>
<div className="recent-path">{project}</div>
</div>
{onRemoveRecentProject && (
<button
className="recent-remove-btn"
onClick={(e) => {
e.stopPropagation();
onRemoveRecentProject(project);
}}
title={t.removeFromList}
>
<Trash2 size={14} />
</button>
)}
</li>
))}
</ul>
@@ -217,6 +249,78 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
</div>
)}
</div>
{/* 右键菜单 | Context Menu */}
{contextMenu && (
<div
className="startup-context-menu-overlay"
onClick={() => setContextMenu(null)}
>
<div
className="startup-context-menu"
style={{ left: contextMenu.x, top: contextMenu.y }}
onClick={(e) => e.stopPropagation()}
>
<button
className="startup-context-menu-item"
onClick={() => {
onRemoveRecentProject?.(contextMenu.project);
setContextMenu(null);
}}
>
<X size={14} />
<span>{t.removeFromList}</span>
</button>
{onDeleteProject && (
<button
className="startup-context-menu-item danger"
onClick={() => {
setDeleteConfirm(contextMenu.project);
setContextMenu(null);
}}
>
<Trash2 size={14} />
<span>{t.deleteProject}</span>
</button>
)}
</div>
</div>
)}
{/* 删除确认对话框 | Delete Confirmation Dialog */}
{deleteConfirm && (
<div className="startup-dialog-overlay">
<div className="startup-dialog">
<div className="startup-dialog-header">
<Trash2 size={20} className="dialog-icon-danger" />
<h3>{t.deleteConfirmTitle}</h3>
</div>
<div className="startup-dialog-body">
<p>{t.deleteConfirmMessage}</p>
<p className="startup-dialog-path">{deleteConfirm}</p>
</div>
<div className="startup-dialog-footer">
<button
className="startup-dialog-btn"
onClick={() => setDeleteConfirm(null)}
>
{t.cancel}
</button>
<button
className="startup-dialog-btn danger"
onClick={async () => {
if (deleteConfirm && onDeleteProject) {
await onDeleteProject(deleteConfirm);
}
setDeleteConfirm(null);
}}
>
{t.delete}
</button>
</div>
</div>
</div>
)}
</div>
);
}