日志面板
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
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 { StartupPage } from './components/StartupPage';
|
||||
import { SceneHierarchy } from './components/SceneHierarchy';
|
||||
import { EntityInspector } from './components/EntityInspector';
|
||||
import { AssetBrowser } from './components/AssetBrowser';
|
||||
import { ConsolePanel } from './components/ConsolePanel';
|
||||
import { DockContainer, DockablePanel } from './components/DockContainer';
|
||||
import { TauriAPI } from './api/tauri';
|
||||
import { useLocale } from './hooks/useLocale';
|
||||
@@ -27,6 +28,7 @@ function App() {
|
||||
const [pluginManager, setPluginManager] = useState<EditorPluginManager | null>(null);
|
||||
const [entityStore, setEntityStore] = useState<EntityStoreService | null>(null);
|
||||
const [messageHub, setMessageHub] = useState<MessageHub | null>(null);
|
||||
const [logService, setLogService] = useState<LogService | null>(null);
|
||||
const { t, locale, changeLocale } = useLocale();
|
||||
const [status, setStatus] = useState(t('header.status.initializing'));
|
||||
const [panels, setPanels] = useState<DockablePanel[]>([]);
|
||||
@@ -48,6 +50,7 @@ function App() {
|
||||
const componentDiscovery = new ComponentDiscoveryService(messageHub);
|
||||
const componentLoader = new ComponentLoaderService(messageHub, componentRegistry);
|
||||
const propertyMetadata = new PropertyMetadataService();
|
||||
const logService = new LogService();
|
||||
|
||||
Core.services.registerInstance(UIRegistry, uiRegistry);
|
||||
Core.services.registerInstance(MessageHub, messageHub);
|
||||
@@ -58,6 +61,7 @@ function App() {
|
||||
Core.services.registerInstance(ComponentDiscoveryService, componentDiscovery);
|
||||
Core.services.registerInstance(ComponentLoaderService, componentLoader);
|
||||
Core.services.registerInstance(PropertyMetadataService, propertyMetadata);
|
||||
Core.services.registerInstance(LogService, logService);
|
||||
|
||||
const pluginMgr = new EditorPluginManager();
|
||||
pluginMgr.initialize(coreInstance, Core.services);
|
||||
@@ -71,6 +75,7 @@ function App() {
|
||||
setPluginManager(pluginMgr);
|
||||
setEntityStore(entityStore);
|
||||
setMessageHub(messageHub);
|
||||
setLogService(logService);
|
||||
setStatus(t('header.status.ready'));
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize editor:', error);
|
||||
@@ -164,7 +169,7 @@ function App() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (projectLoaded && entityStore && messageHub) {
|
||||
if (projectLoaded && entityStore && messageHub && logService) {
|
||||
setPanels([
|
||||
{
|
||||
id: 'scene-hierarchy',
|
||||
@@ -203,21 +208,12 @@ function App() {
|
||||
id: 'console',
|
||||
title: locale === 'zh' ? '控制台' : 'Console',
|
||||
position: 'bottom',
|
||||
content: (
|
||||
<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>
|
||||
),
|
||||
content: <ConsolePanel logService={logService} />,
|
||||
closable: false
|
||||
}
|
||||
]);
|
||||
}
|
||||
}, [projectLoaded, entityStore, messageHub, locale, currentProjectPath, t]);
|
||||
}, [projectLoaded, entityStore, messageHub, logService, locale, currentProjectPath, t]);
|
||||
|
||||
const handlePanelMove = (panelId: string, newPosition: any) => {
|
||||
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/ComponentDiscoveryService';
|
||||
export * from './Services/ComponentLoaderService';
|
||||
export * from './Services/LogService';
|
||||
|
||||
export * from './Types/UITypes';
|
||||
|
||||
Reference in New Issue
Block a user