远程读取日志
This commit is contained in:
@@ -38,6 +38,13 @@ export class DebugManager implements IService, IUpdatable {
|
|||||||
private lastSendTime: number = 0;
|
private lastSendTime: number = 0;
|
||||||
private sendInterval: number;
|
private sendInterval: number;
|
||||||
private isRunning: boolean = false;
|
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(
|
constructor(
|
||||||
@Inject(SceneManager) sceneManager: SceneManager,
|
@Inject(SceneManager) sceneManager: SceneManager,
|
||||||
@@ -68,6 +75,9 @@ export class DebugManager implements IService, IUpdatable {
|
|||||||
const debugFrameRate = this.config.debugFrameRate || 30;
|
const debugFrameRate = this.config.debugFrameRate || 30;
|
||||||
this.sendInterval = 1000 / debugFrameRate;
|
this.sendInterval = 1000 / debugFrameRate;
|
||||||
|
|
||||||
|
// 拦截 console 日志
|
||||||
|
this.interceptConsole();
|
||||||
|
|
||||||
this.start();
|
this.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +101,118 @@ export class DebugManager implements IService, IUpdatable {
|
|||||||
this.webSocketManager.disconnect();
|
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 {
|
public dispose(): void {
|
||||||
this.stop();
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@ import { Viewport } from './components/Viewport';
|
|||||||
import { MenuBar } from './components/MenuBar';
|
import { MenuBar } from './components/MenuBar';
|
||||||
import { DockContainer, DockablePanel } from './components/DockContainer';
|
import { DockContainer, DockablePanel } from './components/DockContainer';
|
||||||
import { TauriAPI } from './api/tauri';
|
import { TauriAPI } from './api/tauri';
|
||||||
|
import { SettingsService } from './services/SettingsService';
|
||||||
import { useLocale } from './hooks/useLocale';
|
import { useLocale } from './hooks/useLocale';
|
||||||
import { en, zh } from './locales';
|
import { en, zh } from './locales';
|
||||||
import { Loader2, Globe } from 'lucide-react';
|
import { Loader2, Globe } from 'lucide-react';
|
||||||
@@ -51,6 +52,7 @@ function App() {
|
|||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
const [pluginUpdateTrigger, setPluginUpdateTrigger] = useState(0);
|
const [pluginUpdateTrigger, setPluginUpdateTrigger] = useState(0);
|
||||||
const [isRemoteConnected, setIsRemoteConnected] = useState(false);
|
const [isRemoteConnected, setIsRemoteConnected] = useState(false);
|
||||||
|
const [isProfilerMode, setIsProfilerMode] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 禁用默认右键菜单
|
// 禁用默认右键菜单
|
||||||
@@ -144,6 +146,12 @@ function App() {
|
|||||||
const logService = new LogService();
|
const logService = new LogService();
|
||||||
const settingsRegistry = new SettingsRegistry();
|
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(UIRegistry, uiRegistry);
|
||||||
Core.services.registerInstance(MessageHub, messageHub);
|
Core.services.registerInstance(MessageHub, messageHub);
|
||||||
Core.services.registerInstance(SerializerRegistry, serializerRegistry);
|
Core.services.registerInstance(SerializerRegistry, serializerRegistry);
|
||||||
@@ -196,11 +204,8 @@ function App() {
|
|||||||
initializeEditor();
|
initializeEditor();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleOpenProject = async () => {
|
const handleOpenRecentProject = async (projectPath: string) => {
|
||||||
try {
|
try {
|
||||||
const projectPath = await TauriAPI.openProjectDialog();
|
|
||||||
if (!projectPath) return;
|
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setLoadingMessage(locale === 'zh' ? '正在打开项目...' : 'Opening project...');
|
setLoadingMessage(locale === 'zh' ? '正在打开项目...' : 'Opening project...');
|
||||||
|
|
||||||
@@ -249,6 +254,9 @@ function App() {
|
|||||||
setStatus(t('header.status.projectOpened'));
|
setStatus(t('header.status.projectOpened'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const settings = SettingsService.getInstance();
|
||||||
|
settings.addRecentProject(projectPath);
|
||||||
|
|
||||||
setCurrentProjectPath(projectPath);
|
setCurrentProjectPath(projectPath);
|
||||||
setProjectLoaded(true);
|
setProjectLoaded(true);
|
||||||
setIsLoading(false);
|
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 () => {
|
const handleCreateProject = async () => {
|
||||||
console.log('Create project not implemented yet');
|
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 = () => {
|
const handleNewScene = () => {
|
||||||
console.log('New scene not implemented yet');
|
console.log('New scene not implemented yet');
|
||||||
};
|
};
|
||||||
@@ -282,6 +307,7 @@ function App() {
|
|||||||
const handleCloseProject = () => {
|
const handleCloseProject = () => {
|
||||||
setProjectLoaded(false);
|
setProjectLoaded(false);
|
||||||
setCurrentProjectPath(null);
|
setCurrentProjectPath(null);
|
||||||
|
setIsProfilerMode(false);
|
||||||
setStatus(t('header.status.ready'));
|
setStatus(t('header.status.ready'));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -304,7 +330,34 @@ function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projectLoaded && entityStore && messageHub && logService && uiRegistry && pluginManager) {
|
if (projectLoaded && entityStore && messageHub && logService && uiRegistry && pluginManager) {
|
||||||
const corePanels: DockablePanel[] = [
|
let corePanels: DockablePanel[];
|
||||||
|
|
||||||
|
if (isProfilerMode) {
|
||||||
|
corePanels = [
|
||||||
|
{
|
||||||
|
id: 'scene-hierarchy',
|
||||||
|
title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy',
|
||||||
|
position: 'left',
|
||||||
|
content: <SceneHierarchy entityStore={entityStore} messageHub={messageHub} />,
|
||||||
|
closable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'inspector',
|
||||||
|
title: locale === 'zh' ? '检视器' : 'Inspector',
|
||||||
|
position: 'right',
|
||||||
|
content: <EntityInspector entityStore={entityStore} messageHub={messageHub} />,
|
||||||
|
closable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'console',
|
||||||
|
title: locale === 'zh' ? '控制台' : 'Console',
|
||||||
|
position: 'bottom',
|
||||||
|
content: <ConsolePanel logService={logService} />,
|
||||||
|
closable: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
corePanels = [
|
||||||
{
|
{
|
||||||
id: 'scene-hierarchy',
|
id: 'scene-hierarchy',
|
||||||
title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy',
|
title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy',
|
||||||
@@ -341,6 +394,7 @@ function App() {
|
|||||||
closable: false
|
closable: false
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const enabledPlugins = pluginManager.getAllPluginMetadata()
|
const enabledPlugins = pluginManager.getAllPluginMetadata()
|
||||||
.filter(p => p.enabled)
|
.filter(p => p.enabled)
|
||||||
@@ -374,7 +428,7 @@ function App() {
|
|||||||
console.log('[App] Loading plugin panels:', pluginPanels);
|
console.log('[App] Loading plugin panels:', pluginPanels);
|
||||||
setPanels([...corePanels, ...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) => {
|
const handlePanelMove = (panelId: string, newPosition: any) => {
|
||||||
setPanels(prevPanels =>
|
setPanels(prevPanels =>
|
||||||
@@ -394,12 +448,17 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!projectLoaded) {
|
if (!projectLoaded) {
|
||||||
|
const settings = SettingsService.getInstance();
|
||||||
|
const recentProjects = settings.getRecentProjects();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StartupPage
|
<StartupPage
|
||||||
onOpenProject={handleOpenProject}
|
onOpenProject={handleOpenProject}
|
||||||
onCreateProject={handleCreateProject}
|
onCreateProject={handleCreateProject}
|
||||||
recentProjects={[]}
|
onOpenRecentProject={handleOpenRecentProject}
|
||||||
|
onProfilerMode={handleProfilerMode}
|
||||||
|
recentProjects={recentProjects}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
/>
|
/>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { LogService, LogEntry } from '@esengine/editor-core';
|
import { LogService, LogEntry } from '@esengine/editor-core';
|
||||||
import { LogLevel } from '@esengine/ecs-framework';
|
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';
|
import '../styles/ConsolePanel.css';
|
||||||
|
|
||||||
interface ConsolePanelProps {
|
interface ConsolePanelProps {
|
||||||
@@ -19,6 +20,8 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
|||||||
LogLevel.Fatal
|
LogLevel.Fatal
|
||||||
]));
|
]));
|
||||||
const [autoScroll, setAutoScroll] = useState(true);
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
|
const [expandedLogs, setExpandedLogs] = useState<Set<number>>(new Set());
|
||||||
|
const [jsonViewerData, setJsonViewerData] = useState<any>(null);
|
||||||
const logContainerRef = useRef<HTMLDivElement>(null);
|
const logContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
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 (
|
||||||
|
<div className="log-message-container">
|
||||||
|
<div className="log-message-text">
|
||||||
|
{shouldTruncate ? (
|
||||||
|
<>
|
||||||
|
{extracted && extracted.prefix && <span>{extracted.prefix} </span>}
|
||||||
|
<span className="log-message-preview">
|
||||||
|
{message.substring(0, MAX_PREVIEW_LENGTH)}...
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>{message}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isJSON && jsonStr && (
|
||||||
|
<button
|
||||||
|
className="log-open-json-btn"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openJsonViewer(jsonStr);
|
||||||
|
}}
|
||||||
|
title="Open in JSON Viewer"
|
||||||
|
>
|
||||||
|
<Maximize2 size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const levelCounts = {
|
const levelCounts = {
|
||||||
[LogLevel.Debug]: logs.filter(l => l.level === LogLevel.Debug).length,
|
[LogLevel.Debug]: logs.filter(l => l.level === LogLevel.Debug).length,
|
||||||
[LogLevel.Info]: logs.filter(l => l.level === LogLevel.Info).length,
|
[LogLevel.Info]: logs.filter(l => l.level === LogLevel.Info).length,
|
||||||
@@ -184,24 +291,46 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
|||||||
<p>No logs to display</p>
|
<p>No logs to display</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filteredLogs.map(log => (
|
filteredLogs.map(log => {
|
||||||
<div key={log.id} className={`log-entry ${getLevelClass(log.level)}`}>
|
const isExpanded = expandedLogs.has(log.id);
|
||||||
|
const shouldShowExpander = log.message.length > 200;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={log.id}
|
||||||
|
className={`log-entry ${getLevelClass(log.level)} ${log.source === 'remote' ? 'log-entry-remote' : ''} ${isExpanded ? 'log-entry-expanded' : ''}`}
|
||||||
|
>
|
||||||
|
{shouldShowExpander && (
|
||||||
|
<div
|
||||||
|
className="log-entry-expander"
|
||||||
|
onClick={() => toggleLogExpand(log.id)}
|
||||||
|
>
|
||||||
|
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="log-entry-icon">
|
<div className="log-entry-icon">
|
||||||
{getLevelIcon(log.level)}
|
{getLevelIcon(log.level)}
|
||||||
</div>
|
</div>
|
||||||
<div className="log-entry-time">
|
<div className="log-entry-time">
|
||||||
{formatTime(log.timestamp)}
|
{formatTime(log.timestamp)}
|
||||||
</div>
|
</div>
|
||||||
<div className="log-entry-source">
|
<div className={`log-entry-source ${log.source === 'remote' ? 'source-remote' : ''}`}>
|
||||||
[{log.source}]
|
[{log.source === 'remote' ? '🌐 Remote' : log.source}]
|
||||||
</div>
|
</div>
|
||||||
<div className="log-entry-message">
|
<div className="log-entry-message">
|
||||||
{log.message}
|
{formatMessage(log.message, isExpanded)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{jsonViewerData && (
|
||||||
|
<JsonViewer
|
||||||
|
data={jsonViewerData}
|
||||||
|
onClose={() => setJsonViewerData(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{!autoScroll && (
|
{!autoScroll && (
|
||||||
<button
|
<button
|
||||||
className="console-scroll-to-bottom"
|
className="console-scroll-to-bottom"
|
||||||
|
|||||||
139
packages/editor-app/src/components/JsonViewer.tsx
Normal file
139
packages/editor-app/src/components/JsonViewer.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { X, ChevronRight, ChevronDown, Copy, Check } from 'lucide-react';
|
||||||
|
import '../styles/JsonViewer.css';
|
||||||
|
|
||||||
|
interface JsonViewerProps {
|
||||||
|
data: any;
|
||||||
|
onClose: () => 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 (
|
||||||
|
<div className="json-viewer-overlay" onClick={onClose}>
|
||||||
|
<div className="json-viewer-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="json-viewer-header">
|
||||||
|
<h3>JSON Viewer</h3>
|
||||||
|
<div className="json-viewer-actions">
|
||||||
|
<button
|
||||||
|
className="json-viewer-btn"
|
||||||
|
onClick={handleCopy}
|
||||||
|
title="Copy JSON"
|
||||||
|
>
|
||||||
|
{copied ? <Check size={16} /> : <Copy size={16} />}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="json-viewer-btn"
|
||||||
|
onClick={onClose}
|
||||||
|
title="Close"
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="json-viewer-content">
|
||||||
|
<JsonTree data={data} name="root" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="json-tree-node">
|
||||||
|
<div
|
||||||
|
className="json-tree-header"
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
>
|
||||||
|
<span className="json-tree-expander">
|
||||||
|
{expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||||
|
</span>
|
||||||
|
<span className="json-tree-key">{name}:</span>
|
||||||
|
<span className={`json-tree-preview ${colorClass}`}>
|
||||||
|
{preview}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{expanded && (
|
||||||
|
<div className="json-tree-children">
|
||||||
|
{isArray ? (
|
||||||
|
value.map((item: any, index: number) => (
|
||||||
|
<JsonTree
|
||||||
|
key={index}
|
||||||
|
data={item}
|
||||||
|
name={`[${index}]`}
|
||||||
|
level={level + 1}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
Object.entries(value).map(([key, val]) => (
|
||||||
|
<JsonTree
|
||||||
|
key={key}
|
||||||
|
data={val}
|
||||||
|
name={key}
|
||||||
|
level={level + 1}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="json-tree-leaf">
|
||||||
|
<span className="json-tree-key">{name}:</span>
|
||||||
|
<span className={`json-tree-value ${colorClass}`}>
|
||||||
|
{type === 'string' ? `"${value}"` : String(value)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return renderValue(data);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
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 { ProfilerService, ProfilerData } from '../services/ProfilerService';
|
||||||
import { SettingsService } from '../services/SettingsService';
|
import { SettingsService } from '../services/SettingsService';
|
||||||
import { Core } from '@esengine/ecs-framework';
|
import { Core } from '@esengine/ecs-framework';
|
||||||
@@ -11,6 +11,7 @@ export function ProfilerDockPanel() {
|
|||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [isServerRunning, setIsServerRunning] = useState(false);
|
const [isServerRunning, setIsServerRunning] = useState(false);
|
||||||
const [port, setPort] = useState('8080');
|
const [port, setPort] = useState('8080');
|
||||||
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const settings = SettingsService.getInstance();
|
const settings = SettingsService.getInstance();
|
||||||
@@ -42,7 +43,9 @@ export function ProfilerDockPanel() {
|
|||||||
|
|
||||||
// 订阅数据更新
|
// 订阅数据更新
|
||||||
const unsubscribe = profilerService.subscribe((data: ProfilerData) => {
|
const unsubscribe = profilerService.subscribe((data: ProfilerData) => {
|
||||||
|
if (!isPaused) {
|
||||||
setProfilerData(data);
|
setProfilerData(data);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 定期检查连接状态
|
// 定期检查连接状态
|
||||||
@@ -58,7 +61,7 @@ export function ProfilerDockPanel() {
|
|||||||
unsubscribe();
|
unsubscribe();
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [isPaused]);
|
||||||
|
|
||||||
const fps = profilerData?.fps || 0;
|
const fps = profilerData?.fps || 0;
|
||||||
const totalFrameTime = profilerData?.totalFrameTime || 0;
|
const totalFrameTime = profilerData?.totalFrameTime || 0;
|
||||||
@@ -74,12 +77,24 @@ export function ProfilerDockPanel() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTogglePause = () => {
|
||||||
|
setIsPaused(!isPaused);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="profiler-dock-panel">
|
<div className="profiler-dock-panel">
|
||||||
<div className="profiler-dock-header">
|
<div className="profiler-dock-header">
|
||||||
<h3>Performance Monitor</h3>
|
<h3>Performance Monitor</h3>
|
||||||
<div className="profiler-dock-header-actions">
|
<div className="profiler-dock-header-actions">
|
||||||
{isConnected && (
|
{isConnected && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="profiler-dock-pause-btn"
|
||||||
|
onClick={handleTogglePause}
|
||||||
|
title={isPaused ? 'Resume data updates' : 'Pause data updates'}
|
||||||
|
>
|
||||||
|
{isPaused ? <Play size={14} /> : <Pause size={14} />}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className="profiler-dock-details-btn"
|
className="profiler-dock-details-btn"
|
||||||
onClick={handleOpenDetails}
|
onClick={handleOpenDetails}
|
||||||
@@ -87,6 +102,7 @@ export function ProfilerDockPanel() {
|
|||||||
>
|
>
|
||||||
<Maximize2 size={14} />
|
<Maximize2 size={14} />
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="profiler-dock-status">
|
<div className="profiler-dock-status">
|
||||||
{isConnected ? (
|
{isConnected ? (
|
||||||
|
|||||||
@@ -56,8 +56,24 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
|||||||
setIsRemoteConnected(connected);
|
setIsRemoteConnected(connected);
|
||||||
|
|
||||||
if (connected && data.entities && data.entities.length > 0) {
|
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([]);
|
setRemoteEntities([]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import '../styles/StartupPage.css';
|
|||||||
interface StartupPageProps {
|
interface StartupPageProps {
|
||||||
onOpenProject: () => void;
|
onOpenProject: () => void;
|
||||||
onCreateProject: () => void;
|
onCreateProject: () => void;
|
||||||
|
onOpenRecentProject?: (projectPath: string) => void;
|
||||||
|
onProfilerMode?: () => void;
|
||||||
recentProjects?: string[];
|
recentProjects?: string[];
|
||||||
locale: 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<string | null>(null);
|
const [hoveredProject, setHoveredProject] = useState<string | null>(null);
|
||||||
|
|
||||||
const translations = {
|
const translations = {
|
||||||
@@ -17,18 +19,22 @@ export function StartupPage({ onOpenProject, onCreateProject, recentProjects = [
|
|||||||
subtitle: 'Professional Game Development Tool',
|
subtitle: 'Professional Game Development Tool',
|
||||||
openProject: 'Open Project',
|
openProject: 'Open Project',
|
||||||
createProject: 'Create New Project',
|
createProject: 'Create New Project',
|
||||||
|
profilerMode: 'Profiler Mode',
|
||||||
recentProjects: 'Recent Projects',
|
recentProjects: 'Recent Projects',
|
||||||
noRecentProjects: 'No recent projects',
|
noRecentProjects: 'No recent projects',
|
||||||
version: 'Version 1.0.0'
|
version: 'Version 1.0.0',
|
||||||
|
comingSoon: 'Coming Soon'
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
title: 'ECS 框架编辑器',
|
title: 'ECS 框架编辑器',
|
||||||
subtitle: '专业游戏开发工具',
|
subtitle: '专业游戏开发工具',
|
||||||
openProject: '打开项目',
|
openProject: '打开项目',
|
||||||
createProject: '创建新项目',
|
createProject: '创建新项目',
|
||||||
|
profilerMode: '性能分析模式',
|
||||||
recentProjects: '最近的项目',
|
recentProjects: '最近的项目',
|
||||||
noRecentProjects: '没有最近的项目',
|
noRecentProjects: '没有最近的项目',
|
||||||
version: '版本 1.0.0'
|
version: '版本 1.0.0',
|
||||||
|
comingSoon: '即将推出'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,18 +49,19 @@ export function StartupPage({ onOpenProject, onCreateProject, recentProjects = [
|
|||||||
|
|
||||||
<div className="startup-content">
|
<div className="startup-content">
|
||||||
<div className="startup-actions">
|
<div className="startup-actions">
|
||||||
<button className="startup-action-btn primary" onClick={onOpenProject}>
|
<button className="startup-action-btn primary" onClick={onProfilerMode}>
|
||||||
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
<path d="M3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V9C21 7.89543 20.1046 7 19 7H13L11 5H5C3.89543 5 3 5.89543 3 7Z" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
<path d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>{t.openProject}</span>
|
<span>{t.profilerMode}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button className="startup-action-btn" onClick={onCreateProject}>
|
<button className="startup-action-btn disabled" disabled title={t.comingSoon}>
|
||||||
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
<path d="M12 5V19M5 12H19" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
<path d="M12 5V19M5 12H19" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>{t.createProject}</span>
|
<span>{t.createProject}</span>
|
||||||
|
<span className="badge-coming-soon">{t.comingSoon}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -70,6 +77,8 @@ export function StartupPage({ onOpenProject, onCreateProject, recentProjects = [
|
|||||||
className={`recent-item ${hoveredProject === project ? 'hovered' : ''}`}
|
className={`recent-item ${hoveredProject === project ? 'hovered' : ''}`}
|
||||||
onMouseEnter={() => setHoveredProject(project)}
|
onMouseEnter={() => setHoveredProject(project)}
|
||||||
onMouseLeave={() => setHoveredProject(null)}
|
onMouseLeave={() => setHoveredProject(null)}
|
||||||
|
onClick={() => onOpenRecentProject?.(project)}
|
||||||
|
style={{ cursor: onOpenRecentProject ? 'pointer' : 'default' }}
|
||||||
>
|
>
|
||||||
<svg className="recent-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
<svg className="recent-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
<path d="M3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V9C21 7.89543 20.1046 7 19 7H13L11 5H5C3.89543 5 3 5.89543 3 7Z" strokeWidth="2"/>
|
<path d="M3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V9C21 7.89543 20.1046 7 19 7H13L11 5H5C3.89543 5 3 5.89543 3 7Z" strokeWidth="2"/>
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export const en: Translations = {
|
|||||||
ready: 'Editor Ready',
|
ready: 'Editor Ready',
|
||||||
failed: 'Initialization Failed',
|
failed: 'Initialization Failed',
|
||||||
projectOpened: 'Project Opened',
|
projectOpened: 'Project Opened',
|
||||||
remoteConnected: 'Remote Game Connected'
|
remoteConnected: 'Remote Game Connected',
|
||||||
|
profilerMode: 'Profiler Mode - Waiting for connection...'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hierarchy: {
|
hierarchy: {
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export const zh: Translations = {
|
|||||||
ready: '编辑器就绪',
|
ready: '编辑器就绪',
|
||||||
failed: '初始化失败',
|
failed: '初始化失败',
|
||||||
projectOpened: '项目已打开',
|
projectOpened: '项目已打开',
|
||||||
remoteConnected: '远程游戏已连接'
|
remoteConnected: '远程游戏已连接',
|
||||||
|
profilerMode: '性能分析模式 - 等待连接...'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hierarchy: {
|
hierarchy: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { SettingsService } from './SettingsService';
|
import { SettingsService } from './SettingsService';
|
||||||
|
import { LogLevel } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
export interface SystemPerformanceData {
|
export interface SystemPerformanceData {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -201,6 +202,8 @@ export class ProfilerService {
|
|||||||
this.handleRawEntityListResponse(message.data);
|
this.handleRawEntityListResponse(message.data);
|
||||||
} else if (message.type === 'get_entity_details_response' && message.data) {
|
} else if (message.type === 'get_entity_details_response' && message.data) {
|
||||||
this.handleEntityDetailsResponse(message.data);
|
this.handleEntityDetailsResponse(message.data);
|
||||||
|
} else if (message.type === 'log' && message.data) {
|
||||||
|
this.handleRemoteLog(message.data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[ProfilerService] Failed to parse message:', 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<string, LogLevel> = {
|
||||||
|
'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 {
|
private createEmptyData(): ProfilerData {
|
||||||
return {
|
return {
|
||||||
totalFrameTime: 0,
|
totalFrameTime: 0,
|
||||||
|
|||||||
@@ -64,4 +64,25 @@ export class SettingsService {
|
|||||||
public getAll(): Record<string, any> {
|
public getAll(): Record<string, any> {
|
||||||
return Object.fromEntries(this.settings);
|
return Object.fromEntries(this.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getRecentProjects(): string[] {
|
||||||
|
return this.get<string[]>('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', []);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,6 +209,41 @@
|
|||||||
opacity: 0.7;
|
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 {
|
.log-entry-message {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
@@ -216,6 +251,72 @@
|
|||||||
padding-top: 2px;
|
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 {
|
.log-entry-debug {
|
||||||
color: var(--color-text-tertiary);
|
color: var(--color-text-tertiary);
|
||||||
}
|
}
|
||||||
|
|||||||
177
packages/editor-app/src/styles/JsonViewer.css
Normal file
177
packages/editor-app/src/styles/JsonViewer.css
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profiler-dock-pause-btn,
|
||||||
.profiler-dock-details-btn {
|
.profiler-dock-details-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -43,12 +44,18 @@
|
|||||||
transition: all var(--transition-fast);
|
transition: all var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profiler-dock-pause-btn:hover,
|
||||||
.profiler-dock-details-btn:hover {
|
.profiler-dock-details-btn:hover {
|
||||||
background: var(--color-bg-hover);
|
background: var(--color-bg-hover);
|
||||||
border-color: var(--color-border-strong);
|
border-color: var(--color-border-strong);
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profiler-dock-pause-btn:active,
|
||||||
|
.profiler-dock-details-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
.profiler-dock-status {
|
.profiler-dock-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.startup-action-btn {
|
.startup-action-btn {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -72,6 +73,28 @@
|
|||||||
border-color: #1177bb;
|
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 {
|
.btn-icon {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
|||||||
@@ -121,6 +121,28 @@ export class LogService implements IService {
|
|||||||
this.notifyListeners(entry);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知监听器
|
* 通知监听器
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user