219 lines
6.7 KiB
TypeScript
219 lines
6.7 KiB
TypeScript
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>
|
||
);
|
||
}
|