rwadurian/backend/mpc-system/services/service-party-app/src/components/DebugConsole.tsx

219 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useRef, useCallback } from 'react';
import styles from './DebugConsole.module.css';
interface LogEntry {
id: number;
timestamp: string;
level: 'info' | 'warn' | 'error' | 'debug';
source: 'main' | 'renderer' | 'grpc' | 'tss' | 'account';
message: string;
}
interface DebugConsoleProps {
isOpen: boolean;
onClose: () => void;
}
let logIdCounter = 0;
export default function DebugConsole({ isOpen, onClose }: DebugConsoleProps) {
const [logs, setLogs] = useState<LogEntry[]>([]);
const [filter, setFilter] = useState<string>('all');
const [autoScroll, setAutoScroll] = useState(true);
const [isPaused, setIsPaused] = useState(false);
const logsEndRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const addLog = useCallback((entry: Omit<LogEntry, 'id' | 'timestamp'>) => {
if (isPaused) return;
const now = new Date();
const ms = now.getMilliseconds().toString().padStart(3, '0');
const newEntry: LogEntry = {
...entry,
id: ++logIdCounter,
timestamp: `${now.toLocaleTimeString('zh-CN', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})}.${ms}`,
};
setLogs(prev => {
const newLogs = [...prev, newEntry];
// 保留最近 500 条日志
if (newLogs.length > 500) {
return newLogs.slice(-500);
}
return newLogs;
});
}, [isPaused]);
useEffect(() => {
if (!isOpen) return;
// 订阅主进程的日志
const handleMainLog = (_event: unknown, data: { level: string; source: string; message: string }) => {
addLog({
level: data.level as LogEntry['level'],
source: data.source as LogEntry['source'],
message: data.message,
});
};
// 使用 IPC 监听日志事件
window.electronAPI?.debug?.subscribeLogs(handleMainLog);
// 拦截 console 方法来捕获前端日志
const originalConsole = {
log: console.log,
warn: console.warn,
error: console.error,
debug: console.debug,
};
console.log = (...args) => {
originalConsole.log(...args);
addLog({ level: 'info', source: 'renderer', message: args.map(String).join(' ') });
};
console.warn = (...args) => {
originalConsole.warn(...args);
addLog({ level: 'warn', source: 'renderer', message: args.map(String).join(' ') });
};
console.error = (...args) => {
originalConsole.error(...args);
addLog({ level: 'error', source: 'renderer', message: args.map(String).join(' ') });
};
console.debug = (...args) => {
originalConsole.debug(...args);
addLog({ level: 'debug', source: 'renderer', message: args.map(String).join(' ') });
};
return () => {
// 恢复原始 console
console.log = originalConsole.log;
console.warn = originalConsole.warn;
console.error = originalConsole.error;
console.debug = originalConsole.debug;
window.electronAPI?.debug?.unsubscribeLogs();
};
}, [isOpen, addLog]);
useEffect(() => {
if (autoScroll && logsEndRef.current) {
logsEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [logs, autoScroll]);
const handleScroll = () => {
if (!containerRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
// 如果用户手动滚动到顶部,暂停自动滚动
setAutoScroll(scrollHeight - scrollTop - clientHeight < 50);
};
const clearLogs = () => {
setLogs([]);
logIdCounter = 0;
};
const filteredLogs = logs.filter(log => {
if (filter === 'all') return true;
if (filter === 'errors') return log.level === 'error' || log.level === 'warn';
return log.source === filter;
});
const getLevelClass = (level: string) => {
switch (level) {
case 'error': return styles.error;
case 'warn': return styles.warn;
case 'debug': return styles.debug;
default: return styles.info;
}
};
const getSourceColor = (source: string) => {
switch (source) {
case 'main': return '#61afef';
case 'grpc': return '#c678dd';
case 'tss': return '#e5c07b';
case 'account': return '#56b6c2';
default: return '#98c379';
}
};
if (!isOpen) return null;
return (
<div className={styles.overlay}>
<div className={styles.console}>
<div className={styles.header}>
<h3>🔧 Debug Console</h3>
<div className={styles.controls}>
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
className={styles.filter}
>
<option value="all">All</option>
<option value="errors">Errors/Warnings</option>
<option value="main">Main Process</option>
<option value="renderer">Renderer</option>
<option value="grpc">gRPC</option>
<option value="tss">TSS</option>
<option value="account">Account</option>
</select>
<button
className={`${styles.btn} ${isPaused ? styles.paused : ''}`}
onClick={() => setIsPaused(!isPaused)}
>
{isPaused ? '▶️ Resume' : '⏸️ Pause'}
</button>
<button
className={`${styles.btn} ${autoScroll ? styles.active : ''}`}
onClick={() => setAutoScroll(!autoScroll)}
>
Auto-scroll
</button>
<button className={styles.btn} onClick={clearLogs}>
🗑 Clear
</button>
<button className={styles.closeBtn} onClick={onClose}>
</button>
</div>
</div>
<div
className={styles.logs}
ref={containerRef}
onScroll={handleScroll}
>
{filteredLogs.length === 0 ? (
<div className={styles.empty}>No logs yet...</div>
) : (
filteredLogs.map(log => (
<div key={log.id} className={`${styles.logEntry} ${getLevelClass(log.level)}`}>
<span className={styles.timestamp}>{log.timestamp}</span>
<span
className={styles.source}
style={{ color: getSourceColor(log.source) }}
>
[{log.source.toUpperCase()}]
</span>
<span className={styles.message}>{log.message}</span>
</div>
))
)}
<div ref={logsEndRef} />
</div>
<div className={styles.footer}>
<span>{filteredLogs.length} logs</span>
<span>Press F12 for DevTools</span>
</div>
</div>
</div>
);
}