From 43bdd7e43bbfe35072d80278fffb272cf025e1db Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Thu, 16 Oct 2025 17:10:22 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=9C=E7=A8=8B=E8=AF=BB=E5=8F=96=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/Utils/Debug/DebugManager.ts | 129 +++++++++++++ packages/editor-app/src/App.tsx | 145 +++++++++----- .../src/components/ConsolePanel.tsx | 161 ++++++++++++++-- .../editor-app/src/components/JsonViewer.tsx | 139 ++++++++++++++ .../src/components/ProfilerDockPanel.tsx | 36 +++- .../src/components/SceneHierarchy.tsx | 20 +- .../editor-app/src/components/StartupPage.tsx | 23 ++- packages/editor-app/src/locales/en.ts | 3 +- packages/editor-app/src/locales/zh.ts | 3 +- .../src/services/ProfilerService.ts | 36 ++++ .../src/services/SettingsService.ts | 21 +++ .../editor-app/src/styles/ConsolePanel.css | 101 ++++++++++ packages/editor-app/src/styles/JsonViewer.css | 177 ++++++++++++++++++ .../src/styles/ProfilerDockPanel.css | 7 + .../editor-app/src/styles/StartupPage.css | 23 +++ .../editor-core/src/Services/LogService.ts | 22 +++ 16 files changed, 966 insertions(+), 80 deletions(-) create mode 100644 packages/editor-app/src/components/JsonViewer.tsx create mode 100644 packages/editor-app/src/styles/JsonViewer.css diff --git a/packages/core/src/Utils/Debug/DebugManager.ts b/packages/core/src/Utils/Debug/DebugManager.ts index 92a57aca..eaced210 100644 --- a/packages/core/src/Utils/Debug/DebugManager.ts +++ b/packages/core/src/Utils/Debug/DebugManager.ts @@ -38,6 +38,13 @@ export class DebugManager implements IService, IUpdatable { private lastSendTime: number = 0; private sendInterval: number; private isRunning: boolean = false; + private originalConsole = { + log: console.log.bind(console), + debug: console.debug.bind(console), + info: console.info.bind(console), + warn: console.warn.bind(console), + error: console.error.bind(console) + }; constructor( @Inject(SceneManager) sceneManager: SceneManager, @@ -68,6 +75,9 @@ export class DebugManager implements IService, IUpdatable { const debugFrameRate = this.config.debugFrameRate || 30; this.sendInterval = 1000 / debugFrameRate; + // 拦截 console 日志 + this.interceptConsole(); + this.start(); } @@ -91,6 +101,118 @@ export class DebugManager implements IService, IUpdatable { this.webSocketManager.disconnect(); } + /** + * 拦截 console 日志并转发到编辑器 + */ + private interceptConsole(): void { + console.log = (...args: unknown[]) => { + this.sendLog('info', this.formatLogMessage(args)); + this.originalConsole.log(...args); + }; + + console.debug = (...args: unknown[]) => { + this.sendLog('debug', this.formatLogMessage(args)); + this.originalConsole.debug(...args); + }; + + console.info = (...args: unknown[]) => { + this.sendLog('info', this.formatLogMessage(args)); + this.originalConsole.info(...args); + }; + + console.warn = (...args: unknown[]) => { + this.sendLog('warn', this.formatLogMessage(args)); + this.originalConsole.warn(...args); + }; + + console.error = (...args: unknown[]) => { + this.sendLog('error', this.formatLogMessage(args)); + this.originalConsole.error(...args); + }; + } + + /** + * 格式化日志消息 + */ + private formatLogMessage(args: unknown[]): string { + return args.map(arg => { + if (typeof arg === 'string') return arg; + if (arg instanceof Error) return `${arg.name}: ${arg.message}`; + if (arg === null) return 'null'; + if (arg === undefined) return 'undefined'; + if (typeof arg === 'object') { + try { + return this.safeStringify(arg, 6); + } catch { + return Object.prototype.toString.call(arg); + } + } + return String(arg); + }).join(' '); + } + + /** + * 安全的 JSON 序列化,支持循环引用和深度限制 + */ + private safeStringify(obj: any, maxDepth: number = 6): string { + const seen = new WeakSet(); + + const stringify = (value: any, depth: number): any => { + if (value === null) return null; + if (value === undefined) return undefined; + if (typeof value !== 'object') return value; + + if (depth >= maxDepth) { + return '[Max Depth Reached]'; + } + + if (seen.has(value)) { + return '[Circular]'; + } + + seen.add(value); + + if (Array.isArray(value)) { + const result = value.map(item => stringify(item, depth + 1)); + seen.delete(value); + return result; + } + + const result: any = {}; + for (const key in value) { + if (Object.prototype.hasOwnProperty.call(value, key)) { + result[key] = stringify(value[key], depth + 1); + } + } + seen.delete(value); + return result; + }; + + return JSON.stringify(stringify(obj, 0)); + } + + /** + * 发送日志到编辑器 + */ + private sendLog(level: string, message: string): void { + if (!this.webSocketManager.getConnectionStatus()) { + return; + } + + try { + this.webSocketManager.send({ + type: 'log', + data: { + level, + message, + timestamp: new Date().toISOString() + } + }); + } catch (error) { + // 静默失败,避免递归日志 + } + } + /** * 更新配置 */ @@ -829,5 +951,12 @@ export class DebugManager implements IService, IUpdatable { */ public dispose(): void { this.stop(); + + // 恢复原始 console 方法 + console.log = this.originalConsole.log; + console.debug = this.originalConsole.debug; + console.info = this.originalConsole.info; + console.warn = this.originalConsole.warn; + console.error = this.originalConsole.error; } } \ No newline at end of file diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index 4733a01b..befa54a7 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -17,6 +17,7 @@ import { Viewport } from './components/Viewport'; import { MenuBar } from './components/MenuBar'; import { DockContainer, DockablePanel } from './components/DockContainer'; import { TauriAPI } from './api/tauri'; +import { SettingsService } from './services/SettingsService'; import { useLocale } from './hooks/useLocale'; import { en, zh } from './locales'; import { Loader2, Globe } from 'lucide-react'; @@ -51,6 +52,7 @@ function App() { const [showSettings, setShowSettings] = useState(false); const [pluginUpdateTrigger, setPluginUpdateTrigger] = useState(0); const [isRemoteConnected, setIsRemoteConnected] = useState(false); + const [isProfilerMode, setIsProfilerMode] = useState(false); useEffect(() => { // 禁用默认右键菜单 @@ -144,6 +146,12 @@ function App() { const logService = new LogService(); const settingsRegistry = new SettingsRegistry(); + // 监听远程日志事件 + window.addEventListener('profiler:remote-log', ((event: CustomEvent) => { + const { level, message, timestamp } = event.detail; + logService.addRemoteLog(level, message, timestamp); + }) as EventListener); + Core.services.registerInstance(UIRegistry, uiRegistry); Core.services.registerInstance(MessageHub, messageHub); Core.services.registerInstance(SerializerRegistry, serializerRegistry); @@ -196,11 +204,8 @@ function App() { initializeEditor(); }, []); - const handleOpenProject = async () => { + const handleOpenRecentProject = async (projectPath: string) => { try { - const projectPath = await TauriAPI.openProjectDialog(); - if (!projectPath) return; - setIsLoading(true); setLoadingMessage(locale === 'zh' ? '正在打开项目...' : 'Opening project...'); @@ -249,6 +254,9 @@ function App() { setStatus(t('header.status.projectOpened')); } + const settings = SettingsService.getInstance(); + settings.addRecentProject(projectPath); + setCurrentProjectPath(projectPath); setProjectLoaded(true); setIsLoading(false); @@ -259,10 +267,27 @@ function App() { } }; + const handleOpenProject = async () => { + try { + const projectPath = await TauriAPI.openProjectDialog(); + if (!projectPath) return; + + await handleOpenRecentProject(projectPath); + } catch (error) { + console.error('Failed to open project dialog:', error); + } + }; + const handleCreateProject = async () => { console.log('Create project not implemented yet'); }; + const handleProfilerMode = async () => { + setIsProfilerMode(true); + setProjectLoaded(true); + setStatus(t('header.status.profilerMode') || 'Profiler Mode - Waiting for connection...'); + }; + const handleNewScene = () => { console.log('New scene not implemented yet'); }; @@ -282,6 +307,7 @@ function App() { const handleCloseProject = () => { setProjectLoaded(false); setCurrentProjectPath(null); + setIsProfilerMode(false); setStatus(t('header.status.ready')); }; @@ -304,43 +330,71 @@ function App() { useEffect(() => { if (projectLoaded && entityStore && messageHub && logService && uiRegistry && pluginManager) { - const corePanels: DockablePanel[] = [ - { - id: 'scene-hierarchy', - title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy', - position: 'left', - content: , - closable: false - }, - { - id: 'inspector', - title: locale === 'zh' ? '检视器' : 'Inspector', - position: 'right', - content: , - closable: false - }, - { - id: 'viewport', - title: locale === 'zh' ? '视口' : 'Viewport', - position: 'center', - content: , - closable: false - }, - { - id: 'assets', - title: locale === 'zh' ? '资产' : 'Assets', - position: 'bottom', - content: , - closable: false - }, - { - id: 'console', - title: locale === 'zh' ? '控制台' : 'Console', - position: 'bottom', - content: , - closable: false - } - ]; + let corePanels: DockablePanel[]; + + if (isProfilerMode) { + corePanels = [ + { + id: 'scene-hierarchy', + title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy', + position: 'left', + content: , + closable: false + }, + { + id: 'inspector', + title: locale === 'zh' ? '检视器' : 'Inspector', + position: 'right', + content: , + closable: false + }, + { + id: 'console', + title: locale === 'zh' ? '控制台' : 'Console', + position: 'bottom', + content: , + closable: false + } + ]; + } else { + corePanels = [ + { + id: 'scene-hierarchy', + title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy', + position: 'left', + content: , + closable: false + }, + { + id: 'inspector', + title: locale === 'zh' ? '检视器' : 'Inspector', + position: 'right', + content: , + closable: false + }, + { + id: 'viewport', + title: locale === 'zh' ? '视口' : 'Viewport', + position: 'center', + content: , + closable: false + }, + { + id: 'assets', + title: locale === 'zh' ? '资产' : 'Assets', + position: 'bottom', + content: , + closable: false + }, + { + id: 'console', + title: locale === 'zh' ? '控制台' : 'Console', + position: 'bottom', + content: , + closable: false + } + ]; + } const enabledPlugins = pluginManager.getAllPluginMetadata() .filter(p => p.enabled) @@ -374,7 +428,7 @@ function App() { console.log('[App] Loading plugin panels:', pluginPanels); setPanels([...corePanels, ...pluginPanels]); } - }, [projectLoaded, entityStore, messageHub, logService, uiRegistry, pluginManager, locale, currentProjectPath, t, pluginUpdateTrigger]); + }, [projectLoaded, entityStore, messageHub, logService, uiRegistry, pluginManager, locale, currentProjectPath, t, pluginUpdateTrigger, isProfilerMode]); const handlePanelMove = (panelId: string, newPosition: any) => { setPanels(prevPanels => @@ -394,12 +448,17 @@ function App() { } if (!projectLoaded) { + const settings = SettingsService.getInstance(); + const recentProjects = settings.getRecentProjects(); + return ( <> {isLoading && ( diff --git a/packages/editor-app/src/components/ConsolePanel.tsx b/packages/editor-app/src/components/ConsolePanel.tsx index 87bae871..396dbc36 100644 --- a/packages/editor-app/src/components/ConsolePanel.tsx +++ b/packages/editor-app/src/components/ConsolePanel.tsx @@ -1,7 +1,8 @@ import { useState, useEffect, useRef } from 'react'; import { LogService, LogEntry } from '@esengine/editor-core'; import { LogLevel } from '@esengine/ecs-framework'; -import { Trash2, AlertCircle, Info, AlertTriangle, XCircle, Bug, Search, Filter } from 'lucide-react'; +import { Trash2, AlertCircle, Info, AlertTriangle, XCircle, Bug, Search, Maximize2, ChevronRight, ChevronDown } from 'lucide-react'; +import { JsonViewer } from './JsonViewer'; import '../styles/ConsolePanel.css'; interface ConsolePanelProps { @@ -19,6 +20,8 @@ export function ConsolePanel({ logService }: ConsolePanelProps) { LogLevel.Fatal ])); const [autoScroll, setAutoScroll] = useState(true); + const [expandedLogs, setExpandedLogs] = useState>(new Set()); + const [jsonViewerData, setJsonViewerData] = useState(null); const logContainerRef = useRef(null); useEffect(() => { @@ -110,6 +113,110 @@ export function ConsolePanel({ logService }: ConsolePanelProps) { }); }; + const toggleLogExpand = (logId: number) => { + const newExpanded = new Set(expandedLogs); + if (newExpanded.has(logId)) { + newExpanded.delete(logId); + } else { + newExpanded.add(logId); + } + setExpandedLogs(newExpanded); + }; + + const extractJSON = (message: string): { prefix: string; json: string; suffix: string } | null => { + const jsonStartChars = ['{', '[']; + let startIndex = -1; + + for (const char of jsonStartChars) { + const index = message.indexOf(char); + if (index !== -1 && (startIndex === -1 || index < startIndex)) { + startIndex = index; + } + } + + if (startIndex === -1) return null; + + for (let endIndex = message.length; endIndex > startIndex; endIndex--) { + const possibleJson = message.substring(startIndex, endIndex); + try { + JSON.parse(possibleJson); + return { + prefix: message.substring(0, startIndex).trim(), + json: possibleJson, + suffix: message.substring(endIndex).trim() + }; + } catch { + continue; + } + } + + return null; + }; + + const tryParseJSON = (message: string): { isJSON: boolean; parsed?: any; jsonStr?: string } => { + try { + const parsed = JSON.parse(message); + return { isJSON: true, parsed, jsonStr: message }; + } catch { + const extracted = extractJSON(message); + if (extracted) { + try { + const parsed = JSON.parse(extracted.json); + return { isJSON: true, parsed, jsonStr: extracted.json }; + } catch { + return { isJSON: false }; + } + } + return { isJSON: false }; + } + }; + + const openJsonViewer = (jsonStr: string) => { + try { + const parsed = JSON.parse(jsonStr); + setJsonViewerData(parsed); + } catch { + console.error('Failed to parse JSON:', jsonStr); + } + }; + + const formatMessage = (message: string, isExpanded: boolean): JSX.Element => { + const MAX_PREVIEW_LENGTH = 200; + const { isJSON, jsonStr } = tryParseJSON(message); + const extracted = extractJSON(message); + + const shouldTruncate = message.length > MAX_PREVIEW_LENGTH && !isExpanded; + + return ( + + + {shouldTruncate ? ( + <> + {extracted && extracted.prefix && {extracted.prefix} } + + {message.substring(0, MAX_PREVIEW_LENGTH)}... + + > + ) : ( + {message} + )} + + {isJSON && jsonStr && ( + { + e.stopPropagation(); + openJsonViewer(jsonStr); + }} + title="Open in JSON Viewer" + > + + + )} + + ); + }; + const levelCounts = { [LogLevel.Debug]: logs.filter(l => l.level === LogLevel.Debug).length, [LogLevel.Info]: logs.filter(l => l.level === LogLevel.Info).length, @@ -184,24 +291,46 @@ export function ConsolePanel({ logService }: ConsolePanelProps) { No logs to display ) : ( - filteredLogs.map(log => ( - - - {getLevelIcon(log.level)} + filteredLogs.map(log => { + const isExpanded = expandedLogs.has(log.id); + const shouldShowExpander = log.message.length > 200; + + return ( + + {shouldShowExpander && ( + toggleLogExpand(log.id)} + > + {isExpanded ? : } + + )} + + {getLevelIcon(log.level)} + + + {formatTime(log.timestamp)} + + + [{log.source === 'remote' ? '🌐 Remote' : log.source}] + + + {formatMessage(log.message, isExpanded)} + - - {formatTime(log.timestamp)} - - - [{log.source}] - - - {log.message} - - - )) + ); + }) )} + {jsonViewerData && ( + setJsonViewerData(null)} + /> + )} {!autoScroll && ( void; +} + +export function JsonViewer({ data, onClose }: JsonViewerProps) { + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + navigator.clipboard.writeText(JSON.stringify(data, null, 2)); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + e.stopPropagation()}> + + JSON Viewer + + + {copied ? : } + + + + + + + + + + + + ); +} + +interface JsonTreeProps { + data: any; + name: string; + level?: number; +} + +function JsonTree({ data, name, level = 0 }: JsonTreeProps) { + const [expanded, setExpanded] = useState(level < 2); + + const getValueType = (value: any): string => { + if (value === null) return 'null'; + if (Array.isArray(value)) return 'array'; + return typeof value; + }; + + const getValueColor = (type: string): string => { + switch (type) { + case 'string': return 'json-string'; + case 'number': return 'json-number'; + case 'boolean': return 'json-boolean'; + case 'null': return 'json-null'; + case 'array': return 'json-array'; + case 'object': return 'json-object'; + default: return ''; + } + }; + + const renderValue = (value: any): JSX.Element => { + const type = getValueType(value); + const colorClass = getValueColor(type); + + if (type === 'object' || type === 'array') { + const isArray = Array.isArray(value); + const keys = Object.keys(value); + const preview = isArray + ? `Array(${value.length})` + : `Object {${keys.length} ${keys.length === 1 ? 'key' : 'keys'}}`; + + return ( + + setExpanded(!expanded)} + > + + {expanded ? : } + + {name}: + + {preview} + + + {expanded && ( + + {isArray ? ( + value.map((item: any, index: number) => ( + + )) + ) : ( + Object.entries(value).map(([key, val]) => ( + + )) + )} + + )} + + ); + } + + return ( + + {name}: + + {type === 'string' ? `"${value}"` : String(value)} + + + ); + }; + + return renderValue(data); +} diff --git a/packages/editor-app/src/components/ProfilerDockPanel.tsx b/packages/editor-app/src/components/ProfilerDockPanel.tsx index 1a53a965..90e77d50 100644 --- a/packages/editor-app/src/components/ProfilerDockPanel.tsx +++ b/packages/editor-app/src/components/ProfilerDockPanel.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2 } from 'lucide-react'; +import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2, Pause, Play } from 'lucide-react'; import { ProfilerService, ProfilerData } from '../services/ProfilerService'; import { SettingsService } from '../services/SettingsService'; import { Core } from '@esengine/ecs-framework'; @@ -11,6 +11,7 @@ export function ProfilerDockPanel() { const [isConnected, setIsConnected] = useState(false); const [isServerRunning, setIsServerRunning] = useState(false); const [port, setPort] = useState('8080'); + const [isPaused, setIsPaused] = useState(false); useEffect(() => { const settings = SettingsService.getInstance(); @@ -42,7 +43,9 @@ export function ProfilerDockPanel() { // 订阅数据更新 const unsubscribe = profilerService.subscribe((data: ProfilerData) => { - setProfilerData(data); + if (!isPaused) { + setProfilerData(data); + } }); // 定期检查连接状态 @@ -58,7 +61,7 @@ export function ProfilerDockPanel() { unsubscribe(); clearInterval(interval); }; - }, []); + }, [isPaused]); const fps = profilerData?.fps || 0; const totalFrameTime = profilerData?.totalFrameTime || 0; @@ -74,19 +77,32 @@ export function ProfilerDockPanel() { } }; + const handleTogglePause = () => { + setIsPaused(!isPaused); + }; + return ( Performance Monitor {isConnected && ( - - - + <> + + {isPaused ? : } + + + + + > )} {isConnected ? ( diff --git a/packages/editor-app/src/components/SceneHierarchy.tsx b/packages/editor-app/src/components/SceneHierarchy.tsx index b580f315..77fd3349 100644 --- a/packages/editor-app/src/components/SceneHierarchy.tsx +++ b/packages/editor-app/src/components/SceneHierarchy.tsx @@ -56,8 +56,24 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps) setIsRemoteConnected(connected); if (connected && data.entities && data.entities.length > 0) { - setRemoteEntities(data.entities); - } else { + // 只在实体列表发生实质性变化时才更新 + setRemoteEntities(prev => { + if (prev.length !== data.entities!.length) { + return data.entities!; + } + + // 检查实体ID和名称是否变化 + const hasChanged = data.entities!.some((entity, index) => { + const prevEntity = prev[index]; + return !prevEntity || + prevEntity.id !== entity.id || + prevEntity.name !== entity.name || + prevEntity.componentCount !== entity.componentCount; + }); + + return hasChanged ? data.entities! : prev; + }); + } else if (!connected) { setRemoteEntities([]); } }); diff --git a/packages/editor-app/src/components/StartupPage.tsx b/packages/editor-app/src/components/StartupPage.tsx index 7b1c2609..96762c1c 100644 --- a/packages/editor-app/src/components/StartupPage.tsx +++ b/packages/editor-app/src/components/StartupPage.tsx @@ -4,11 +4,13 @@ import '../styles/StartupPage.css'; interface StartupPageProps { onOpenProject: () => void; onCreateProject: () => void; + onOpenRecentProject?: (projectPath: string) => void; + onProfilerMode?: () => void; recentProjects?: string[]; locale: string; } -export function StartupPage({ onOpenProject, onCreateProject, recentProjects = [], locale }: StartupPageProps) { +export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onProfilerMode, recentProjects = [], locale }: StartupPageProps) { const [hoveredProject, setHoveredProject] = useState(null); const translations = { @@ -17,18 +19,22 @@ export function StartupPage({ onOpenProject, onCreateProject, recentProjects = [ subtitle: 'Professional Game Development Tool', openProject: 'Open Project', createProject: 'Create New Project', + profilerMode: 'Profiler Mode', recentProjects: 'Recent Projects', noRecentProjects: 'No recent projects', - version: 'Version 1.0.0' + version: 'Version 1.0.0', + comingSoon: 'Coming Soon' }, zh: { title: 'ECS 框架编辑器', subtitle: '专业游戏开发工具', openProject: '打开项目', createProject: '创建新项目', + profilerMode: '性能分析模式', recentProjects: '最近的项目', noRecentProjects: '没有最近的项目', - version: '版本 1.0.0' + version: '版本 1.0.0', + comingSoon: '即将推出' } }; @@ -43,18 +49,19 @@ export function StartupPage({ onOpenProject, onCreateProject, recentProjects = [ - + - + - {t.openProject} + {t.profilerMode} - + {t.createProject} + {t.comingSoon} @@ -70,6 +77,8 @@ export function StartupPage({ onOpenProject, onCreateProject, recentProjects = [ className={`recent-item ${hoveredProject === project ? 'hovered' : ''}`} onMouseEnter={() => setHoveredProject(project)} onMouseLeave={() => setHoveredProject(null)} + onClick={() => onOpenRecentProject?.(project)} + style={{ cursor: onOpenRecentProject ? 'pointer' : 'default' }} > diff --git a/packages/editor-app/src/locales/en.ts b/packages/editor-app/src/locales/en.ts index 33c50102..de706c1d 100644 --- a/packages/editor-app/src/locales/en.ts +++ b/packages/editor-app/src/locales/en.ts @@ -15,7 +15,8 @@ export const en: Translations = { ready: 'Editor Ready', failed: 'Initialization Failed', projectOpened: 'Project Opened', - remoteConnected: 'Remote Game Connected' + remoteConnected: 'Remote Game Connected', + profilerMode: 'Profiler Mode - Waiting for connection...' } }, hierarchy: { diff --git a/packages/editor-app/src/locales/zh.ts b/packages/editor-app/src/locales/zh.ts index 1acbc527..932e4b36 100644 --- a/packages/editor-app/src/locales/zh.ts +++ b/packages/editor-app/src/locales/zh.ts @@ -15,7 +15,8 @@ export const zh: Translations = { ready: '编辑器就绪', failed: '初始化失败', projectOpened: '项目已打开', - remoteConnected: '远程游戏已连接' + remoteConnected: '远程游戏已连接', + profilerMode: '性能分析模式 - 等待连接...' } }, hierarchy: { diff --git a/packages/editor-app/src/services/ProfilerService.ts b/packages/editor-app/src/services/ProfilerService.ts index f1380b1c..ef5790d0 100644 --- a/packages/editor-app/src/services/ProfilerService.ts +++ b/packages/editor-app/src/services/ProfilerService.ts @@ -1,5 +1,6 @@ import { invoke } from '@tauri-apps/api/core'; import { SettingsService } from './SettingsService'; +import { LogLevel } from '@esengine/ecs-framework'; export interface SystemPerformanceData { name: string; @@ -201,6 +202,8 @@ export class ProfilerService { this.handleRawEntityListResponse(message.data); } else if (message.type === 'get_entity_details_response' && message.data) { this.handleEntityDetailsResponse(message.data); + } else if (message.type === 'log' && message.data) { + this.handleRemoteLog(message.data); } } catch (error) { console.error('[ProfilerService] Failed to parse message:', error); @@ -343,6 +346,39 @@ export class ProfilerService { })); } + private handleRemoteLog(data: any): void { + if (!data) { + return; + } + + const levelMap: Record = { + 'debug': LogLevel.Debug, + 'info': LogLevel.Info, + 'warn': LogLevel.Warn, + 'error': LogLevel.Error, + 'fatal': LogLevel.Fatal + }; + + const level = levelMap[data.level?.toLowerCase() || 'info'] || LogLevel.Info; + + let message = data.message || ''; + if (typeof message === 'object') { + try { + message = JSON.stringify(message, null, 2); + } catch { + message = String(message); + } + } + + window.dispatchEvent(new CustomEvent('profiler:remote-log', { + detail: { + level, + message, + timestamp: data.timestamp ? new Date(data.timestamp) : new Date() + } + })); + } + private createEmptyData(): ProfilerData { return { totalFrameTime: 0, diff --git a/packages/editor-app/src/services/SettingsService.ts b/packages/editor-app/src/services/SettingsService.ts index 29d0fd6f..059ba931 100644 --- a/packages/editor-app/src/services/SettingsService.ts +++ b/packages/editor-app/src/services/SettingsService.ts @@ -64,4 +64,25 @@ export class SettingsService { public getAll(): Record { return Object.fromEntries(this.settings); } + + public getRecentProjects(): string[] { + return this.get('recentProjects', []); + } + + public addRecentProject(projectPath: string): void { + const recentProjects = this.getRecentProjects(); + const filtered = recentProjects.filter(p => p !== projectPath); + const updated = [projectPath, ...filtered].slice(0, 10); + this.set('recentProjects', updated); + } + + public removeRecentProject(projectPath: string): void { + const recentProjects = this.getRecentProjects(); + const filtered = recentProjects.filter(p => p !== projectPath); + this.set('recentProjects', filtered); + } + + public clearRecentProjects(): void { + this.set('recentProjects', []); + } } diff --git a/packages/editor-app/src/styles/ConsolePanel.css b/packages/editor-app/src/styles/ConsolePanel.css index 112f4418..581307f7 100644 --- a/packages/editor-app/src/styles/ConsolePanel.css +++ b/packages/editor-app/src/styles/ConsolePanel.css @@ -209,6 +209,41 @@ opacity: 0.7; } +.log-entry-source.source-remote { + color: #4a9eff; + opacity: 1; + font-weight: 600; +} + +.log-entry-remote { + border-left: 2px solid #4a9eff; + background: rgba(74, 158, 255, 0.05); +} + +.log-entry-expander { + display: flex; + align-items: center; + padding-top: 2px; + cursor: pointer; + color: var(--color-text-secondary); + flex-shrink: 0; + transition: color var(--transition-fast); +} + +.log-entry-expander:hover { + color: var(--color-text-primary); +} + +.log-entry-expanded { + flex-direction: column; + align-items: flex-start; +} + +.log-entry-expanded .log-entry-message { + padding-left: 22px; + width: 100%; +} + .log-entry-message { flex: 1; color: var(--color-text-primary); @@ -216,6 +251,72 @@ padding-top: 2px; } +.log-message-container { + display: flex; + align-items: flex-start; + gap: 8px; + width: 100%; +} + +.log-message-preview { + opacity: 0.9; + flex: 1; +} + +.log-open-json-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px; + background: var(--color-primary); + border: none; + border-radius: var(--radius-sm); + color: white; + cursor: pointer; + opacity: 0.7; + transition: all var(--transition-fast); + flex-shrink: 0; +} + +.log-open-json-btn:hover { + opacity: 1; + transform: scale(1.1); +} + +.log-message-json { + margin: 4px 0 0 0; + padding: 8px; + background: rgba(0, 0, 0, 0.3); + border-radius: var(--radius-sm); + border: 1px solid var(--color-border-default); + font-family: var(--font-family-mono); + font-size: 11px; + line-height: 1.5; + overflow: auto; + white-space: pre; + color: #a0e7a0; + max-height: 400px; +} + +.log-message-json::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.log-message-json::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.2); + border-radius: 4px; +} + +.log-message-json::-webkit-scrollbar-thumb { + background: rgba(160, 231, 160, 0.3); + border-radius: 4px; +} + +.log-message-json::-webkit-scrollbar-thumb:hover { + background: rgba(160, 231, 160, 0.5); +} + .log-entry-debug { color: var(--color-text-tertiary); } diff --git a/packages/editor-app/src/styles/JsonViewer.css b/packages/editor-app/src/styles/JsonViewer.css new file mode 100644 index 00000000..6dddea4a --- /dev/null +++ b/packages/editor-app/src/styles/JsonViewer.css @@ -0,0 +1,177 @@ +.json-viewer-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + backdrop-filter: blur(4px); +} + +.json-viewer-modal { + background: var(--color-bg-elevated); + border: 1px solid var(--color-border-default); + border-radius: var(--radius-md); + box-shadow: var(--shadow-xl); + width: 90%; + max-width: 1200px; + height: 85vh; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.json-viewer-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid var(--color-border-default); + background: var(--color-bg-base); +} + +.json-viewer-header h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: var(--color-text-primary); +} + +.json-viewer-actions { + display: flex; + gap: 8px; +} + +.json-viewer-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px; + background: transparent; + border: none; + border-radius: var(--radius-sm); + color: var(--color-text-secondary); + cursor: pointer; + transition: all var(--transition-fast); +} + +.json-viewer-btn:hover { + background: var(--color-bg-hover); + color: var(--color-text-primary); +} + +.json-viewer-content { + flex: 1; + overflow: auto; + padding: 16px; + font-family: var(--font-family-mono); + font-size: 12px; + line-height: 1.6; +} + +.json-tree-node { + margin-bottom: 4px; +} + +.json-tree-header { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border-radius: var(--radius-sm); + cursor: pointer; + transition: background var(--transition-fast); + user-select: none; +} + +.json-tree-header:hover { + background: var(--color-bg-hover); +} + +.json-tree-expander { + display: flex; + align-items: center; + color: var(--color-text-tertiary); + flex-shrink: 0; +} + +.json-tree-key { + color: #9cdcfe; + font-weight: 500; + margin-right: 4px; +} + +.json-tree-preview { + color: var(--color-text-tertiary); + font-style: italic; +} + +.json-tree-children { + margin-left: 20px; + padding-left: 12px; + border-left: 1px solid var(--color-border-subtle); +} + +.json-tree-leaf { + display: flex; + align-items: baseline; + gap: 6px; + padding: 2px 8px; + border-radius: var(--radius-sm); + transition: background var(--transition-fast); +} + +.json-tree-leaf:hover { + background: var(--color-bg-hover); +} + +.json-tree-value { + font-family: var(--font-family-mono); +} + +.json-string { + color: #ce9178; +} + +.json-number { + color: #b5cea8; +} + +.json-boolean { + color: #569cd6; +} + +.json-null { + color: #569cd6; + font-style: italic; +} + +.json-array { + color: #4ec9b0; +} + +.json-object { + color: #4ec9b0; +} + +.json-viewer-content::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +.json-viewer-content::-webkit-scrollbar-track { + background: var(--color-bg-base); +} + +.json-viewer-content::-webkit-scrollbar-thumb { + background: var(--color-border-default); + border-radius: 5px; +} + +.json-viewer-content::-webkit-scrollbar-thumb:hover { + background: var(--color-text-secondary); +} diff --git a/packages/editor-app/src/styles/ProfilerDockPanel.css b/packages/editor-app/src/styles/ProfilerDockPanel.css index eaf44489..c122b510 100644 --- a/packages/editor-app/src/styles/ProfilerDockPanel.css +++ b/packages/editor-app/src/styles/ProfilerDockPanel.css @@ -28,6 +28,7 @@ gap: 8px; } +.profiler-dock-pause-btn, .profiler-dock-details-btn { display: flex; align-items: center; @@ -43,12 +44,18 @@ transition: all var(--transition-fast); } +.profiler-dock-pause-btn:hover, .profiler-dock-details-btn:hover { background: var(--color-bg-hover); border-color: var(--color-border-strong); color: var(--color-text-primary); } +.profiler-dock-pause-btn:active, +.profiler-dock-details-btn:active { + transform: scale(0.95); +} + .profiler-dock-status { display: flex; align-items: center; diff --git a/packages/editor-app/src/styles/StartupPage.css b/packages/editor-app/src/styles/StartupPage.css index 8c408065..5292fbb2 100644 --- a/packages/editor-app/src/styles/StartupPage.css +++ b/packages/editor-app/src/styles/StartupPage.css @@ -42,6 +42,7 @@ } .startup-action-btn { + position: relative; display: flex; align-items: center; gap: 12px; @@ -72,6 +73,28 @@ border-color: #1177bb; } +.startup-action-btn.disabled { + background-color: #252526; + border-color: #3e3e42; + color: #6e6e6e; + cursor: not-allowed; + opacity: 0.5; +} + +.startup-action-btn.disabled:hover { + background-color: #252526; + border-color: #3e3e42; +} + +.badge-coming-soon { + margin-left: auto; + font-size: 10px; + padding: 2px 6px; + background-color: #3e3e42; + border-radius: 2px; + color: #858585; +} + .btn-icon { width: 20px; height: 20px; diff --git a/packages/editor-core/src/Services/LogService.ts b/packages/editor-core/src/Services/LogService.ts index ae9602a8..e0bc8385 100644 --- a/packages/editor-core/src/Services/LogService.ts +++ b/packages/editor-core/src/Services/LogService.ts @@ -121,6 +121,28 @@ export class LogService implements IService { this.notifyListeners(entry); } + /** + * 添加远程日志(从远程游戏接收) + */ + public addRemoteLog(level: LogLevel, message: string, timestamp?: Date): void { + const entry: LogEntry = { + id: this.nextId++, + timestamp: timestamp || new Date(), + level, + source: 'remote', + message, + args: [] + }; + + this.logs.push(entry); + + if (this.logs.length > this.maxLogs) { + this.logs.shift(); + } + + this.notifyListeners(entry); + } + /** * 通知监听器 */
No logs to display