2025-10-16 20:18:20 +08:00
|
|
|
|
const { BrowserWindow } = require('electron');
|
|
|
|
|
|
const path = require('path');
|
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 日志窗口管理器
|
|
|
|
|
|
* 显示 MySQL 和 Spring Boot 的实时日志
|
|
|
|
|
|
*/
|
|
|
|
|
|
class LogWindowManager {
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
this.logWindow = null;
|
|
|
|
|
|
this.logs = [];
|
|
|
|
|
|
this.maxLogs = 1000; // 最多保留1000条日志
|
2025-11-26 08:50:22 +08:00
|
|
|
|
|
|
|
|
|
|
// 初始化日志文件路径
|
|
|
|
|
|
this.initLogFile();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 初始化日志文件路径(按天滚动)
|
|
|
|
|
|
*/
|
|
|
|
|
|
initLogFile() {
|
|
|
|
|
|
// 开发环境:项目根目录的 logs 文件夹
|
|
|
|
|
|
// 打包后:应用根目录的 logs 文件夹
|
|
|
|
|
|
const isDev = !process.resourcesPath;
|
|
|
|
|
|
const baseDir = isDev
|
|
|
|
|
|
? path.join(__dirname, '..')
|
|
|
|
|
|
: path.dirname(process.resourcesPath);
|
|
|
|
|
|
|
|
|
|
|
|
this.logsDir = path.join(baseDir, 'logs');
|
|
|
|
|
|
|
|
|
|
|
|
// 确保 logs 目录存在
|
|
|
|
|
|
if (!fs.existsSync(this.logsDir)) {
|
|
|
|
|
|
fs.mkdirSync(this.logsDir, { recursive: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成当天的日志文件名:startup-YYYYMMDD.log
|
|
|
|
|
|
const today = new Date();
|
|
|
|
|
|
const dateStr = today.getFullYear() +
|
|
|
|
|
|
String(today.getMonth() + 1).padStart(2, '0') +
|
|
|
|
|
|
String(today.getDate()).padStart(2, '0');
|
|
|
|
|
|
this.logFilePath = path.join(this.logsDir, `startup-${dateStr}.log`);
|
|
|
|
|
|
|
|
|
|
|
|
console.log('[LogWindowManager] Log file:', this.logFilePath);
|
|
|
|
|
|
|
|
|
|
|
|
// 写入启动标记
|
|
|
|
|
|
this.writeToFile(new Date().toISOString().replace('T', ' ').substring(0, 19), 'SYSTEM', '=' .repeat(80));
|
|
|
|
|
|
this.writeToFile(new Date().toISOString().replace('T', ' ').substring(0, 19), 'SYSTEM', 'NPQS9100 应用启动');
|
|
|
|
|
|
this.writeToFile(new Date().toISOString().replace('T', ' ').substring(0, 19), 'SYSTEM', '=' .repeat(80));
|
|
|
|
|
|
|
|
|
|
|
|
// 清理超过30天的旧日志
|
|
|
|
|
|
this.cleanOldLogs(30);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 清理旧日志文件
|
|
|
|
|
|
* @param {number} days - 保留天数
|
|
|
|
|
|
*/
|
|
|
|
|
|
cleanOldLogs(days) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
const maxAge = days * 24 * 60 * 60 * 1000; // 转换为毫秒
|
|
|
|
|
|
|
|
|
|
|
|
const files = fs.readdirSync(this.logsDir);
|
|
|
|
|
|
let deletedCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
files.forEach(file => {
|
|
|
|
|
|
if (file.startsWith('startup-') && file.endsWith('.log')) {
|
|
|
|
|
|
const filePath = path.join(this.logsDir, file);
|
|
|
|
|
|
const stats = fs.statSync(filePath);
|
|
|
|
|
|
const age = now - stats.mtimeMs;
|
|
|
|
|
|
|
|
|
|
|
|
if (age > maxAge) {
|
|
|
|
|
|
fs.unlinkSync(filePath);
|
|
|
|
|
|
deletedCount++;
|
|
|
|
|
|
console.log(`[LogWindowManager] Deleted old log: ${file}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (deletedCount > 0) {
|
|
|
|
|
|
console.log(`[LogWindowManager] Cleaned up ${deletedCount} old log file(s)`);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[LogWindowManager] Failed to clean old logs:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 写入日志到文件
|
|
|
|
|
|
*/
|
|
|
|
|
|
writeToFile(timestamp, type, message) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const logLine = `[${timestamp}] [${type.toUpperCase()}] ${message}\n`;
|
|
|
|
|
|
fs.appendFileSync(this.logFilePath, logLine, 'utf-8');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[LogWindowManager] Failed to write log to file:', error);
|
|
|
|
|
|
}
|
2025-10-16 20:18:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建日志窗口
|
|
|
|
|
|
*/
|
|
|
|
|
|
createLogWindow() {
|
2025-11-26 08:50:22 +08:00
|
|
|
|
const isDev = !process.resourcesPath;
|
|
|
|
|
|
const iconPath = isDev
|
|
|
|
|
|
? path.join(__dirname, '..', 'public', 'images', 'icon.png')
|
|
|
|
|
|
: path.join(process.resourcesPath, 'app.asar.unpacked', 'public', 'images', 'icon.png');
|
|
|
|
|
|
|
2025-10-16 20:18:20 +08:00
|
|
|
|
this.logWindow = new BrowserWindow({
|
|
|
|
|
|
width: 900,
|
|
|
|
|
|
height: 600,
|
|
|
|
|
|
title: 'NPQS9100 - 服务日志',
|
|
|
|
|
|
backgroundColor: '#1e1e1e',
|
2025-11-26 08:50:22 +08:00
|
|
|
|
icon: iconPath,
|
2025-10-16 20:18:20 +08:00
|
|
|
|
webPreferences: {
|
|
|
|
|
|
nodeIntegration: true,
|
|
|
|
|
|
contextIsolation: false
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 加载日志页面
|
|
|
|
|
|
const logHtml = this.generateLogHTML();
|
|
|
|
|
|
this.logWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(logHtml)}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 窗口关闭事件 - 只清理引用,不退出应用
|
|
|
|
|
|
this.logWindow.on('closed', () => {
|
|
|
|
|
|
console.log('[LogWindow] Log window closed by user');
|
|
|
|
|
|
this.logWindow = null;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 防止日志窗口关闭时退出应用(但允许隐藏)
|
|
|
|
|
|
this.closeHandler = (event) => {
|
|
|
|
|
|
// 只是隐藏窗口,不是真正关闭
|
|
|
|
|
|
// 这样可以随时再打开
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
this.logWindow.hide();
|
|
|
|
|
|
console.log('[LogWindow] Log window hidden');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
this.logWindow.on('close', this.closeHandler);
|
|
|
|
|
|
|
|
|
|
|
|
return this.logWindow;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成日志HTML页面
|
|
|
|
|
|
*/
|
|
|
|
|
|
generateLogHTML() {
|
|
|
|
|
|
return `
|
|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html>
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<title>NPQS9100 服务日志</title>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
* {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
body {
|
|
|
|
|
|
font-family: 'Consolas', 'Courier New', monospace;
|
|
|
|
|
|
background: #1e1e1e;
|
|
|
|
|
|
color: #d4d4d4;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.header {
|
|
|
|
|
|
background: #2d2d30;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.title {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #4ec9b0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.controls button {
|
|
|
|
|
|
background: #0e639c;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
padding: 5px 10px;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
margin-left: 5px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.controls button:hover {
|
|
|
|
|
|
background: #1177bb;
|
|
|
|
|
|
}
|
|
|
|
|
|
.log-container {
|
|
|
|
|
|
background: #1e1e1e;
|
|
|
|
|
|
border: 1px solid #3e3e42;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
height: calc(100vh - 70px);
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
.log-entry {
|
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
word-wrap: break-word;
|
|
|
|
|
|
}
|
|
|
|
|
|
.log-mysql { color: #4ec9b0; }
|
|
|
|
|
|
.log-java { color: #dcdcaa; }
|
|
|
|
|
|
.log-system { color: #569cd6; }
|
|
|
|
|
|
.log-error { color: #f48771; }
|
|
|
|
|
|
.log-warn { color: #ce9178; }
|
|
|
|
|
|
.log-success { color: #608b4e; }
|
|
|
|
|
|
.timestamp {
|
|
|
|
|
|
color: #858585;
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
::-webkit-scrollbar {
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
::-webkit-scrollbar-track {
|
|
|
|
|
|
background: #1e1e1e;
|
|
|
|
|
|
}
|
|
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background: #424242;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
|
background: #4e4e4e;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
<div class="title">📝 NPQS9100 服务日志监控</div>
|
|
|
|
|
|
<div class="controls">
|
|
|
|
|
|
<button onclick="clearLogs()">清空日志</button>
|
|
|
|
|
|
<button onclick="toggleAutoScroll()">自动滚动: <span id="autoScrollStatus">开</span></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="log-container" id="logContainer"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
const { ipcRenderer } = require('electron');
|
|
|
|
|
|
const logContainer = document.getElementById('logContainer');
|
|
|
|
|
|
let autoScroll = true;
|
|
|
|
|
|
|
|
|
|
|
|
// 接收日志
|
|
|
|
|
|
ipcRenderer.on('log-message', (event, data) => {
|
|
|
|
|
|
addLog(data);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function addLog(data) {
|
|
|
|
|
|
const entry = document.createElement('div');
|
|
|
|
|
|
entry.className = 'log-entry';
|
|
|
|
|
|
|
|
|
|
|
|
const timestamp = document.createElement('span');
|
|
|
|
|
|
timestamp.className = 'timestamp';
|
|
|
|
|
|
timestamp.textContent = data.timestamp;
|
|
|
|
|
|
entry.appendChild(timestamp);
|
|
|
|
|
|
|
|
|
|
|
|
const message = document.createElement('span');
|
|
|
|
|
|
message.className = \`log-\${data.type}\`;
|
|
|
|
|
|
message.textContent = data.message;
|
|
|
|
|
|
entry.appendChild(message);
|
|
|
|
|
|
|
|
|
|
|
|
logContainer.appendChild(entry);
|
|
|
|
|
|
|
|
|
|
|
|
// 自动滚动到底部
|
|
|
|
|
|
if (autoScroll) {
|
|
|
|
|
|
logContainer.scrollTop = logContainer.scrollHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 限制日志条数
|
|
|
|
|
|
while (logContainer.children.length > 1000) {
|
|
|
|
|
|
logContainer.removeChild(logContainer.firstChild);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearLogs() {
|
|
|
|
|
|
logContainer.innerHTML = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleAutoScroll() {
|
|
|
|
|
|
autoScroll = !autoScroll;
|
|
|
|
|
|
document.getElementById('autoScrollStatus').textContent = autoScroll ? '开' : '关';
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 添加日志
|
|
|
|
|
|
*/
|
|
|
|
|
|
addLog(type, message) {
|
2025-11-26 08:50:22 +08:00
|
|
|
|
const now = new Date();
|
|
|
|
|
|
const timestamp = now.toLocaleTimeString();
|
|
|
|
|
|
const fullTimestamp = now.toISOString().replace('T', ' ').substring(0, 19);
|
|
|
|
|
|
|
2025-10-16 20:18:20 +08:00
|
|
|
|
const logEntry = {
|
|
|
|
|
|
timestamp,
|
|
|
|
|
|
type,
|
|
|
|
|
|
message
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
this.logs.push(logEntry);
|
|
|
|
|
|
|
|
|
|
|
|
// 限制日志数量
|
|
|
|
|
|
if (this.logs.length > this.maxLogs) {
|
|
|
|
|
|
this.logs.shift();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-26 08:50:22 +08:00
|
|
|
|
// 写入文件(使用完整时间戳)
|
|
|
|
|
|
this.writeToFile(fullTimestamp, type, message);
|
|
|
|
|
|
|
2025-10-16 20:18:20 +08:00
|
|
|
|
// 发送到窗口
|
|
|
|
|
|
if (this.logWindow && !this.logWindow.isDestroyed()) {
|
|
|
|
|
|
this.logWindow.webContents.send('log-message', logEntry);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 同时输出到控制台
|
|
|
|
|
|
console.log(`[${type.toUpperCase()}] ${message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 显示日志窗口
|
|
|
|
|
|
*/
|
|
|
|
|
|
show() {
|
|
|
|
|
|
if (!this.logWindow || this.logWindow.isDestroyed()) {
|
|
|
|
|
|
// 窗口已被销毁,重新创建
|
|
|
|
|
|
console.log('[LogWindow] Recreating log window...');
|
|
|
|
|
|
this.createLogWindow();
|
|
|
|
|
|
|
|
|
|
|
|
// 重新发送历史日志
|
|
|
|
|
|
this.logs.forEach(log => {
|
|
|
|
|
|
this.logWindow.webContents.send('log-message', log);
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.logWindow.show();
|
|
|
|
|
|
this.logWindow.focus();
|
|
|
|
|
|
console.log('[LogWindow] Log window shown');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 隐藏日志窗口
|
|
|
|
|
|
*/
|
|
|
|
|
|
hide() {
|
|
|
|
|
|
if (this.logWindow && !this.logWindow.isDestroyed()) {
|
|
|
|
|
|
this.logWindow.hide();
|
|
|
|
|
|
console.log('[LogWindow] Log window hidden');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查日志窗口是否可见
|
|
|
|
|
|
*/
|
|
|
|
|
|
isVisible() {
|
|
|
|
|
|
return this.logWindow && !this.logWindow.isDestroyed() && this.logWindow.isVisible();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 切换日志窗口显示/隐藏
|
|
|
|
|
|
*/
|
|
|
|
|
|
toggle() {
|
|
|
|
|
|
if (this.isVisible()) {
|
|
|
|
|
|
this.hide();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.show();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 关闭日志窗口(真正销毁)
|
|
|
|
|
|
*/
|
|
|
|
|
|
close() {
|
|
|
|
|
|
if (this.logWindow && !this.logWindow.isDestroyed()) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 移除 close 事件监听,允许真正关闭
|
|
|
|
|
|
if (this.closeHandler) {
|
|
|
|
|
|
this.logWindow.removeListener('close', this.closeHandler);
|
|
|
|
|
|
}
|
|
|
|
|
|
this.logWindow.removeAllListeners('close');
|
|
|
|
|
|
this.logWindow.removeAllListeners('closed');
|
|
|
|
|
|
|
|
|
|
|
|
// 强制销毁窗口
|
|
|
|
|
|
this.logWindow.destroy();
|
|
|
|
|
|
console.log('[LogWindow] Log window destroyed');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('[LogWindow] Error closing log window:', error);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.logWindow = null;
|
|
|
|
|
|
this.closeHandler = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取所有日志
|
|
|
|
|
|
*/
|
|
|
|
|
|
getLogs() {
|
|
|
|
|
|
return this.logs;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 清空日志
|
|
|
|
|
|
*/
|
|
|
|
|
|
clearLogs() {
|
|
|
|
|
|
this.logs = [];
|
|
|
|
|
|
if (this.logWindow && !this.logWindow.isDestroyed()) {
|
|
|
|
|
|
this.logWindow.webContents.send('clear-logs');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = LogWindowManager;
|
|
|
|
|
|
|