123 lines
5.2 KiB
HTML
123 lines
5.2 KiB
HTML
|
|
{% extends "base.html" %}
|
|||
|
|
{% block title %}自动签名系统 - 日志{% endblock %}
|
|||
|
|
{% block content %}
|
|||
|
|
<div style="display:flex;flex-direction:column;height:100%;">
|
|||
|
|
<div class="log-toolbar">
|
|||
|
|
<span style="font-size:13px;font-weight:600;color:#1a237e;"><i class="bi bi-terminal"></i> 系统日志</span>
|
|||
|
|
<select class="form-select" id="logSourceFilter">
|
|||
|
|
<option value="all">全部来源</option>
|
|||
|
|
<option value="前端操作">前端操作</option>
|
|||
|
|
<option value="外部API">外部API</option>
|
|||
|
|
</select>
|
|||
|
|
<select class="form-select" id="logLevelFilter">
|
|||
|
|
<option value="all">全部级别</option>
|
|||
|
|
<option value="INFO">INFO</option>
|
|||
|
|
<option value="WARN">WARN</option>
|
|||
|
|
<option value="ERROR">ERROR</option>
|
|||
|
|
</select>
|
|||
|
|
<input type="text" class="form-control" id="logKeyword" placeholder="关键字搜索" style="width:200px;">
|
|||
|
|
<div class="form-check form-check-inline ms-2">
|
|||
|
|
<input class="form-check-input" type="checkbox" id="autoScroll" checked>
|
|||
|
|
<label class="form-check-label" for="autoScroll" style="font-size:12px;">自动滚动</label>
|
|||
|
|
</div>
|
|||
|
|
<button class="btn btn-outline-secondary btn-sm" id="btnClearLog"><i class="bi bi-trash"></i> 清空</button>
|
|||
|
|
<button class="btn btn-outline-danger btn-sm" id="btnLocateError"><i class="bi bi-exclamation-triangle"></i> 定位异常</button>
|
|||
|
|
<span class="ms-auto" style="font-size:12px;color:#757575;" id="logCount">0 条</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="log-content" id="logContent"></div>
|
|||
|
|
</div>
|
|||
|
|
{% endblock %}
|
|||
|
|
|
|||
|
|
{% block extra_js %}
|
|||
|
|
<script>
|
|||
|
|
(function() {
|
|||
|
|
const logContent = document.getElementById('logContent');
|
|||
|
|
const logSourceFilter = document.getElementById('logSourceFilter');
|
|||
|
|
const logLevelFilter = document.getElementById('logLevelFilter');
|
|||
|
|
const logKeyword = document.getElementById('logKeyword');
|
|||
|
|
const autoScroll = document.getElementById('autoScroll');
|
|||
|
|
const logCountEl = document.getElementById('logCount');
|
|||
|
|
let allLogs = [];
|
|||
|
|
let logId = 0;
|
|||
|
|
|
|||
|
|
function addLogEntry(level, source, message) {
|
|||
|
|
logId++;
|
|||
|
|
const entry = { id: logId, level, source, message, time: new Date() };
|
|||
|
|
allLogs.push(entry);
|
|||
|
|
renderEntry(entry);
|
|||
|
|
logCountEl.textContent = allLogs.length + ' 条';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderEntry(entry) {
|
|||
|
|
if (!matchFilter(entry)) return;
|
|||
|
|
const div = document.createElement('div');
|
|||
|
|
div.className = `log-entry level-${entry.level}`;
|
|||
|
|
div.dataset.id = entry.id;
|
|||
|
|
div.dataset.level = entry.level;
|
|||
|
|
div.dataset.source = entry.source;
|
|||
|
|
|
|||
|
|
const sourceClass = entry.source === '外部API' ? 'log-source-api' : 'log-source-frontend';
|
|||
|
|
const timeStr = entry.time.toLocaleTimeString('zh-CN', { hour12: false });
|
|||
|
|
|
|||
|
|
div.innerHTML = `<span class="log-timestamp">[${timeStr}]</span> <span class="${sourceClass}">[${entry.source}]</span> <span style="font-weight:600;">[${entry.level}]</span> ${escapeHtml(entry.message)}`;
|
|||
|
|
logContent.appendChild(div);
|
|||
|
|
|
|||
|
|
if (autoScroll.checked) {
|
|||
|
|
logContent.scrollTop = logContent.scrollHeight;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function matchFilter(entry) {
|
|||
|
|
const src = logSourceFilter.value;
|
|||
|
|
const lvl = logLevelFilter.value;
|
|||
|
|
const kw = logKeyword.value.trim().toLowerCase();
|
|||
|
|
if (src !== 'all' && entry.source !== src) return false;
|
|||
|
|
if (lvl !== 'all' && entry.level !== lvl) return false;
|
|||
|
|
if (kw && !entry.message.toLowerCase().includes(kw)) return false;
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function rerenderAll() {
|
|||
|
|
logContent.innerHTML = '';
|
|||
|
|
allLogs.forEach(entry => renderEntry(entry));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
logSourceFilter.addEventListener('change', rerenderAll);
|
|||
|
|
logLevelFilter.addEventListener('change', rerenderAll);
|
|||
|
|
logKeyword.addEventListener('input', rerenderAll);
|
|||
|
|
|
|||
|
|
document.getElementById('btnClearLog').addEventListener('click', function() {
|
|||
|
|
allLogs = [];
|
|||
|
|
logContent.innerHTML = '';
|
|||
|
|
logCountEl.textContent = '0 条';
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.getElementById('btnLocateError').addEventListener('click', function() {
|
|||
|
|
const errorEntries = logContent.querySelectorAll('.level-ERROR');
|
|||
|
|
if (errorEntries.length > 0) {
|
|||
|
|
errorEntries.forEach(e => e.classList.remove('highlight-error'));
|
|||
|
|
const last = errorEntries[errorEntries.length - 1];
|
|||
|
|
last.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|||
|
|
last.classList.add('highlight-error');
|
|||
|
|
} else {
|
|||
|
|
alert('没有发现异常日志');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const evtSource = new EventSource('/api/logs/stream');
|
|||
|
|
evtSource.onmessage = function(event) {
|
|||
|
|
try {
|
|||
|
|
const data = JSON.parse(event.data);
|
|||
|
|
addLogEntry(data.level || 'INFO', data.source || '系统', data.message || '');
|
|||
|
|
} catch (e) {
|
|||
|
|
addLogEntry('INFO', '系统', event.data);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
evtSource.onerror = function() {
|
|||
|
|
addLogEntry('WARN', '系统', '日志流连接断开,5秒后重连...');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
addLogEntry('INFO', '系统', '日志页面已加载,正在监听系统日志...');
|
|||
|
|
})();
|
|||
|
|
</script>
|
|||
|
|
{% endblock %}
|