日志面板
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Core, Scene } from '@esengine/ecs-framework';
|
import { Core, Scene } from '@esengine/ecs-framework';
|
||||||
import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry, LocaleService, ProjectService, ComponentDiscoveryService, ComponentLoaderService, PropertyMetadataService } from '@esengine/editor-core';
|
import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry, LocaleService, ProjectService, ComponentDiscoveryService, ComponentLoaderService, PropertyMetadataService, LogService } from '@esengine/editor-core';
|
||||||
import { SceneInspectorPlugin } from './plugins/SceneInspectorPlugin';
|
import { SceneInspectorPlugin } from './plugins/SceneInspectorPlugin';
|
||||||
import { StartupPage } from './components/StartupPage';
|
import { StartupPage } from './components/StartupPage';
|
||||||
import { SceneHierarchy } from './components/SceneHierarchy';
|
import { SceneHierarchy } from './components/SceneHierarchy';
|
||||||
import { EntityInspector } from './components/EntityInspector';
|
import { EntityInspector } from './components/EntityInspector';
|
||||||
import { AssetBrowser } from './components/AssetBrowser';
|
import { AssetBrowser } from './components/AssetBrowser';
|
||||||
|
import { ConsolePanel } from './components/ConsolePanel';
|
||||||
import { DockContainer, DockablePanel } from './components/DockContainer';
|
import { DockContainer, DockablePanel } from './components/DockContainer';
|
||||||
import { TauriAPI } from './api/tauri';
|
import { TauriAPI } from './api/tauri';
|
||||||
import { useLocale } from './hooks/useLocale';
|
import { useLocale } from './hooks/useLocale';
|
||||||
@@ -27,6 +28,7 @@ function App() {
|
|||||||
const [pluginManager, setPluginManager] = useState<EditorPluginManager | null>(null);
|
const [pluginManager, setPluginManager] = useState<EditorPluginManager | null>(null);
|
||||||
const [entityStore, setEntityStore] = useState<EntityStoreService | null>(null);
|
const [entityStore, setEntityStore] = useState<EntityStoreService | null>(null);
|
||||||
const [messageHub, setMessageHub] = useState<MessageHub | null>(null);
|
const [messageHub, setMessageHub] = useState<MessageHub | null>(null);
|
||||||
|
const [logService, setLogService] = useState<LogService | null>(null);
|
||||||
const { t, locale, changeLocale } = useLocale();
|
const { t, locale, changeLocale } = useLocale();
|
||||||
const [status, setStatus] = useState(t('header.status.initializing'));
|
const [status, setStatus] = useState(t('header.status.initializing'));
|
||||||
const [panels, setPanels] = useState<DockablePanel[]>([]);
|
const [panels, setPanels] = useState<DockablePanel[]>([]);
|
||||||
@@ -48,6 +50,7 @@ function App() {
|
|||||||
const componentDiscovery = new ComponentDiscoveryService(messageHub);
|
const componentDiscovery = new ComponentDiscoveryService(messageHub);
|
||||||
const componentLoader = new ComponentLoaderService(messageHub, componentRegistry);
|
const componentLoader = new ComponentLoaderService(messageHub, componentRegistry);
|
||||||
const propertyMetadata = new PropertyMetadataService();
|
const propertyMetadata = new PropertyMetadataService();
|
||||||
|
const logService = new LogService();
|
||||||
|
|
||||||
Core.services.registerInstance(UIRegistry, uiRegistry);
|
Core.services.registerInstance(UIRegistry, uiRegistry);
|
||||||
Core.services.registerInstance(MessageHub, messageHub);
|
Core.services.registerInstance(MessageHub, messageHub);
|
||||||
@@ -58,6 +61,7 @@ function App() {
|
|||||||
Core.services.registerInstance(ComponentDiscoveryService, componentDiscovery);
|
Core.services.registerInstance(ComponentDiscoveryService, componentDiscovery);
|
||||||
Core.services.registerInstance(ComponentLoaderService, componentLoader);
|
Core.services.registerInstance(ComponentLoaderService, componentLoader);
|
||||||
Core.services.registerInstance(PropertyMetadataService, propertyMetadata);
|
Core.services.registerInstance(PropertyMetadataService, propertyMetadata);
|
||||||
|
Core.services.registerInstance(LogService, logService);
|
||||||
|
|
||||||
const pluginMgr = new EditorPluginManager();
|
const pluginMgr = new EditorPluginManager();
|
||||||
pluginMgr.initialize(coreInstance, Core.services);
|
pluginMgr.initialize(coreInstance, Core.services);
|
||||||
@@ -71,6 +75,7 @@ function App() {
|
|||||||
setPluginManager(pluginMgr);
|
setPluginManager(pluginMgr);
|
||||||
setEntityStore(entityStore);
|
setEntityStore(entityStore);
|
||||||
setMessageHub(messageHub);
|
setMessageHub(messageHub);
|
||||||
|
setLogService(logService);
|
||||||
setStatus(t('header.status.ready'));
|
setStatus(t('header.status.ready'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize editor:', error);
|
console.error('Failed to initialize editor:', error);
|
||||||
@@ -164,7 +169,7 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projectLoaded && entityStore && messageHub) {
|
if (projectLoaded && entityStore && messageHub && logService) {
|
||||||
setPanels([
|
setPanels([
|
||||||
{
|
{
|
||||||
id: 'scene-hierarchy',
|
id: 'scene-hierarchy',
|
||||||
@@ -203,21 +208,12 @@ function App() {
|
|||||||
id: 'console',
|
id: 'console',
|
||||||
title: locale === 'zh' ? '控制台' : 'Console',
|
title: locale === 'zh' ? '控制台' : 'Console',
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
content: (
|
content: <ConsolePanel logService={logService} />,
|
||||||
<div style={{ padding: '12px', height: '100%', overflow: 'auto' }}>
|
|
||||||
<h4 style={{ margin: '0 0 8px 0', fontSize: '12px', color: '#ffffff' }}>
|
|
||||||
{locale === 'zh' ? '控制台' : 'Console'}
|
|
||||||
</h4>
|
|
||||||
<p style={{ fontSize: '12px', color: '#858585' }}>
|
|
||||||
{locale === 'zh' ? '控制台输出将显示在这里...' : 'Console output will appear here...'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
closable: false
|
closable: false
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}, [projectLoaded, entityStore, messageHub, locale, currentProjectPath, t]);
|
}, [projectLoaded, entityStore, messageHub, logService, locale, currentProjectPath, t]);
|
||||||
|
|
||||||
const handlePanelMove = (panelId: string, newPosition: any) => {
|
const handlePanelMove = (panelId: string, newPosition: any) => {
|
||||||
setPanels(prevPanels =>
|
setPanels(prevPanels =>
|
||||||
|
|||||||
220
packages/editor-app/src/components/ConsolePanel.tsx
Normal file
220
packages/editor-app/src/components/ConsolePanel.tsx
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
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 '../styles/ConsolePanel.css';
|
||||||
|
|
||||||
|
interface ConsolePanelProps {
|
||||||
|
logService: LogService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConsolePanel({ logService }: ConsolePanelProps) {
|
||||||
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
|
const [filter, setFilter] = useState('');
|
||||||
|
const [levelFilter, setLevelFilter] = useState<Set<LogLevel>>(new Set([
|
||||||
|
LogLevel.Debug,
|
||||||
|
LogLevel.Info,
|
||||||
|
LogLevel.Warn,
|
||||||
|
LogLevel.Error,
|
||||||
|
LogLevel.Fatal
|
||||||
|
]));
|
||||||
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
|
const logContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLogs(logService.getLogs());
|
||||||
|
|
||||||
|
const unsubscribe = logService.subscribe((entry) => {
|
||||||
|
setLogs(prev => [...prev, entry]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
}, [logService]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoScroll && logContainerRef.current) {
|
||||||
|
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [logs, autoScroll]);
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
logService.clear();
|
||||||
|
setLogs([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (logContainerRef.current) {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = logContainerRef.current;
|
||||||
|
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
||||||
|
setAutoScroll(isAtBottom);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleLevelFilter = (level: LogLevel) => {
|
||||||
|
const newFilter = new Set(levelFilter);
|
||||||
|
if (newFilter.has(level)) {
|
||||||
|
newFilter.delete(level);
|
||||||
|
} else {
|
||||||
|
newFilter.add(level);
|
||||||
|
}
|
||||||
|
setLevelFilter(newFilter);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredLogs = logs.filter(log => {
|
||||||
|
if (!levelFilter.has(log.level)) return false;
|
||||||
|
if (filter && !log.message.toLowerCase().includes(filter.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getLevelIcon = (level: LogLevel) => {
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel.Debug:
|
||||||
|
return <Bug size={14} />;
|
||||||
|
case LogLevel.Info:
|
||||||
|
return <Info size={14} />;
|
||||||
|
case LogLevel.Warn:
|
||||||
|
return <AlertTriangle size={14} />;
|
||||||
|
case LogLevel.Error:
|
||||||
|
case LogLevel.Fatal:
|
||||||
|
return <XCircle size={14} />;
|
||||||
|
default:
|
||||||
|
return <AlertCircle size={14} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLevelClass = (level: LogLevel): string => {
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel.Debug:
|
||||||
|
return 'log-entry-debug';
|
||||||
|
case LogLevel.Info:
|
||||||
|
return 'log-entry-info';
|
||||||
|
case LogLevel.Warn:
|
||||||
|
return 'log-entry-warn';
|
||||||
|
case LogLevel.Error:
|
||||||
|
case LogLevel.Fatal:
|
||||||
|
return 'log-entry-error';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (date: Date): string => {
|
||||||
|
return date.toLocaleTimeString('en-US', {
|
||||||
|
hour12: false,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
fractionalSecondDigits: 3
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const levelCounts = {
|
||||||
|
[LogLevel.Debug]: logs.filter(l => l.level === LogLevel.Debug).length,
|
||||||
|
[LogLevel.Info]: logs.filter(l => l.level === LogLevel.Info).length,
|
||||||
|
[LogLevel.Warn]: logs.filter(l => l.level === LogLevel.Warn).length,
|
||||||
|
[LogLevel.Error]: logs.filter(l => l.level === LogLevel.Error || l.level === LogLevel.Fatal).length
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="console-panel">
|
||||||
|
<div className="console-toolbar">
|
||||||
|
<div className="console-toolbar-left">
|
||||||
|
<button
|
||||||
|
className="console-btn"
|
||||||
|
onClick={handleClear}
|
||||||
|
title="Clear console"
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
|
<div className="console-search">
|
||||||
|
<Search size={14} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Filter logs..."
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="console-toolbar-right">
|
||||||
|
<button
|
||||||
|
className={`console-filter-btn ${levelFilter.has(LogLevel.Debug) ? 'active' : ''}`}
|
||||||
|
onClick={() => toggleLevelFilter(LogLevel.Debug)}
|
||||||
|
title="Debug"
|
||||||
|
>
|
||||||
|
<Bug size={14} />
|
||||||
|
{levelCounts[LogLevel.Debug] > 0 && <span>{levelCounts[LogLevel.Debug]}</span>}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`console-filter-btn ${levelFilter.has(LogLevel.Info) ? 'active' : ''}`}
|
||||||
|
onClick={() => toggleLevelFilter(LogLevel.Info)}
|
||||||
|
title="Info"
|
||||||
|
>
|
||||||
|
<Info size={14} />
|
||||||
|
{levelCounts[LogLevel.Info] > 0 && <span>{levelCounts[LogLevel.Info]}</span>}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`console-filter-btn ${levelFilter.has(LogLevel.Warn) ? 'active' : ''}`}
|
||||||
|
onClick={() => toggleLevelFilter(LogLevel.Warn)}
|
||||||
|
title="Warnings"
|
||||||
|
>
|
||||||
|
<AlertTriangle size={14} />
|
||||||
|
{levelCounts[LogLevel.Warn] > 0 && <span>{levelCounts[LogLevel.Warn]}</span>}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`console-filter-btn ${levelFilter.has(LogLevel.Error) ? 'active' : ''}`}
|
||||||
|
onClick={() => toggleLevelFilter(LogLevel.Error)}
|
||||||
|
title="Errors"
|
||||||
|
>
|
||||||
|
<XCircle size={14} />
|
||||||
|
{levelCounts[LogLevel.Error] > 0 && <span>{levelCounts[LogLevel.Error]}</span>}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="console-content"
|
||||||
|
ref={logContainerRef}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
>
|
||||||
|
{filteredLogs.length === 0 ? (
|
||||||
|
<div className="console-empty">
|
||||||
|
<AlertCircle size={32} />
|
||||||
|
<p>No logs to display</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredLogs.map(log => (
|
||||||
|
<div key={log.id} className={`log-entry ${getLevelClass(log.level)}`}>
|
||||||
|
<div className="log-entry-icon">
|
||||||
|
{getLevelIcon(log.level)}
|
||||||
|
</div>
|
||||||
|
<div className="log-entry-time">
|
||||||
|
{formatTime(log.timestamp)}
|
||||||
|
</div>
|
||||||
|
<div className="log-entry-source">
|
||||||
|
[{log.source}]
|
||||||
|
</div>
|
||||||
|
<div className="log-entry-message">
|
||||||
|
{log.message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!autoScroll && (
|
||||||
|
<button
|
||||||
|
className="console-scroll-to-bottom"
|
||||||
|
onClick={() => {
|
||||||
|
if (logContainerRef.current) {
|
||||||
|
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
|
||||||
|
setAutoScroll(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
↓ Scroll to bottom
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
282
packages/editor-app/src/styles/ConsolePanel.css
Normal file
282
packages/editor-app/src/styles/ConsolePanel.css
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
.console-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--color-bg-base);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--color-bg-elevated);
|
||||||
|
border-bottom: 1px solid var(--color-border-default);
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-toolbar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-btn:hover {
|
||||||
|
background: var(--color-bg-hover);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex: 1;
|
||||||
|
max-width: 300px;
|
||||||
|
background: var(--color-bg-inset);
|
||||||
|
border: 1px solid var(--color-border-default);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 4px 8px;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-search:focus-within {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-search input {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: var(--font-family-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-search input::placeholder {
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-filter-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-filter-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: var(--color-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-filter-btn.active {
|
||||||
|
opacity: 1;
|
||||||
|
border-color: currentColor;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-filter-btn:nth-child(1) {
|
||||||
|
color: #858585;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-filter-btn:nth-child(1).active {
|
||||||
|
color: #a0a0a0;
|
||||||
|
border-color: #858585;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-filter-btn:nth-child(2) {
|
||||||
|
color: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-filter-btn:nth-child(2).active {
|
||||||
|
color: #6eb3ff;
|
||||||
|
border-color: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-filter-btn:nth-child(3) {
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-filter-btn:nth-child(3).active {
|
||||||
|
color: #ffd54f;
|
||||||
|
border-color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-filter-btn:nth-child(4) {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-filter-btn:nth-child(4).active {
|
||||||
|
color: #ef5350;
|
||||||
|
border-color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-filter-btn span {
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: var(--font-family-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
font-family: var(--font-family-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-empty p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-bottom: 1px solid var(--color-border-subtle);
|
||||||
|
transition: background-color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry-time {
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
font-size: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-top: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry-source {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-top: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry-message {
|
||||||
|
flex: 1;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
word-break: break-word;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry-debug {
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry-debug .log-entry-icon {
|
||||||
|
color: #858585;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry-info .log-entry-icon {
|
||||||
|
color: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry-warn {
|
||||||
|
background: rgba(255, 193, 7, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry-warn .log-entry-icon {
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry-error {
|
||||||
|
background: rgba(244, 67, 54, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry-error .log-entry-icon {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-scroll-to-bottom {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 12px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--color-text-inverse);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-scroll-to-bottom:hover {
|
||||||
|
background: var(--color-primary-hover);
|
||||||
|
transform: translateX(-50%) translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-scroll-to-bottom:active {
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.console-btn,
|
||||||
|
.console-filter-btn,
|
||||||
|
.log-entry,
|
||||||
|
.console-scroll-to-bottom {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
187
packages/editor-core/src/Services/LogService.ts
Normal file
187
packages/editor-core/src/Services/LogService.ts
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import type { IService } from '@esengine/ecs-framework';
|
||||||
|
import { Injectable, LogLevel } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
export interface LogEntry {
|
||||||
|
id: number;
|
||||||
|
timestamp: Date;
|
||||||
|
level: LogLevel;
|
||||||
|
source: string;
|
||||||
|
message: string;
|
||||||
|
args: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LogListener = (entry: LogEntry) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑器日志服务
|
||||||
|
*
|
||||||
|
* 捕获框架和用户代码的所有日志输出,并提供给UI层展示
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class LogService implements IService {
|
||||||
|
private logs: LogEntry[] = [];
|
||||||
|
private listeners: Set<LogListener> = new Set();
|
||||||
|
private nextId = 0;
|
||||||
|
private maxLogs = 1000;
|
||||||
|
|
||||||
|
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() {
|
||||||
|
this.interceptConsole();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拦截控制台输出
|
||||||
|
*/
|
||||||
|
private interceptConsole(): void {
|
||||||
|
console.log = (...args: unknown[]) => {
|
||||||
|
this.addLog(LogLevel.Info, 'console', this.formatMessage(args), args);
|
||||||
|
this.originalConsole.log(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.debug = (...args: unknown[]) => {
|
||||||
|
this.addLog(LogLevel.Debug, 'console', this.formatMessage(args), args);
|
||||||
|
this.originalConsole.debug(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.info = (...args: unknown[]) => {
|
||||||
|
this.addLog(LogLevel.Info, 'console', this.formatMessage(args), args);
|
||||||
|
this.originalConsole.info(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.warn = (...args: unknown[]) => {
|
||||||
|
this.addLog(LogLevel.Warn, 'console', this.formatMessage(args), args);
|
||||||
|
this.originalConsole.warn(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.error = (...args: unknown[]) => {
|
||||||
|
this.addLog(LogLevel.Error, 'console', this.formatMessage(args), args);
|
||||||
|
this.originalConsole.error(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('error', (event) => {
|
||||||
|
this.addLog(
|
||||||
|
LogLevel.Error,
|
||||||
|
'error',
|
||||||
|
event.message,
|
||||||
|
[event.error]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('unhandledrejection', (event) => {
|
||||||
|
this.addLog(
|
||||||
|
LogLevel.Error,
|
||||||
|
'promise',
|
||||||
|
`Unhandled Promise Rejection: ${event.reason}`,
|
||||||
|
[event.reason]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化消息
|
||||||
|
*/
|
||||||
|
private formatMessage(args: unknown[]): string {
|
||||||
|
return args.map(arg => {
|
||||||
|
if (typeof arg === 'string') return arg;
|
||||||
|
if (arg instanceof Error) return arg.message;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(arg);
|
||||||
|
} catch {
|
||||||
|
return String(arg);
|
||||||
|
}
|
||||||
|
}).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加日志
|
||||||
|
*/
|
||||||
|
private addLog(level: LogLevel, source: string, message: string, args: unknown[]): void {
|
||||||
|
const entry: LogEntry = {
|
||||||
|
id: this.nextId++,
|
||||||
|
timestamp: new Date(),
|
||||||
|
level,
|
||||||
|
source,
|
||||||
|
message,
|
||||||
|
args
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logs.push(entry);
|
||||||
|
|
||||||
|
if (this.logs.length > this.maxLogs) {
|
||||||
|
this.logs.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notifyListeners(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知监听器
|
||||||
|
*/
|
||||||
|
private notifyListeners(entry: LogEntry): void {
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
try {
|
||||||
|
listener(entry);
|
||||||
|
} catch (error) {
|
||||||
|
this.originalConsole.error('Error in log listener:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有日志
|
||||||
|
*/
|
||||||
|
public getLogs(): LogEntry[] {
|
||||||
|
return [...this.logs];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空日志
|
||||||
|
*/
|
||||||
|
public clear(): void {
|
||||||
|
this.logs = [];
|
||||||
|
this.notifyListeners({
|
||||||
|
id: -1,
|
||||||
|
timestamp: new Date(),
|
||||||
|
level: LogLevel.Info,
|
||||||
|
source: 'system',
|
||||||
|
message: 'Logs cleared',
|
||||||
|
args: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅日志更新
|
||||||
|
*/
|
||||||
|
public subscribe(listener: LogListener): () => void {
|
||||||
|
this.listeners.add(listener);
|
||||||
|
return () => this.listeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最大日志数量
|
||||||
|
*/
|
||||||
|
public setMaxLogs(max: number): void {
|
||||||
|
this.maxLogs = max;
|
||||||
|
while (this.logs.length > this.maxLogs) {
|
||||||
|
this.logs.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
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;
|
||||||
|
|
||||||
|
this.listeners.clear();
|
||||||
|
this.logs = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,5 +17,6 @@ export * from './Services/PropertyMetadata';
|
|||||||
export * from './Services/ProjectService';
|
export * from './Services/ProjectService';
|
||||||
export * from './Services/ComponentDiscoveryService';
|
export * from './Services/ComponentDiscoveryService';
|
||||||
export * from './Services/ComponentLoaderService';
|
export * from './Services/ComponentLoaderService';
|
||||||
|
export * from './Services/LogService';
|
||||||
|
|
||||||
export * from './Types/UITypes';
|
export * from './Types/UITypes';
|
||||||
|
|||||||
Reference in New Issue
Block a user