501 lines
15 KiB
JavaScript
501 lines
15 KiB
JavaScript
/**
|
||
* 日志管理界面JavaScript
|
||
*
|
||
* 处理日志页面的交互功能
|
||
*/
|
||
|
||
(function($) {
|
||
'use strict';
|
||
|
||
/**
|
||
* 日志管理器
|
||
*/
|
||
var LogManager = {
|
||
|
||
/**
|
||
* 初始化
|
||
*/
|
||
init: function() {
|
||
this.bindEvents();
|
||
this.setupAutoRefresh();
|
||
},
|
||
|
||
/**
|
||
* 绑定事件
|
||
*/
|
||
bindEvents: function() {
|
||
// 刷新日志
|
||
$('#refresh-logs').on('click', this.refreshLogs.bind(this));
|
||
|
||
// 清空日志
|
||
$('#clear-logs').on('click', this.clearLogs.bind(this));
|
||
|
||
// 下载日志
|
||
$('#download-logs').on('click', this.downloadLogs.bind(this));
|
||
|
||
// 日志行点击展开详情
|
||
$(document).on('click', '.log-table tbody tr', this.toggleLogDetails.bind(this));
|
||
|
||
// 实时搜索
|
||
$('input[name="search"]').on('input', this.debounce(this.performSearch.bind(this), 500));
|
||
},
|
||
|
||
/**
|
||
* 刷新日志
|
||
*/
|
||
refreshLogs: function() {
|
||
location.reload();
|
||
},
|
||
|
||
/**
|
||
* 清空日志
|
||
*/
|
||
clearLogs: function() {
|
||
if (!confirm(yoone_logs_params.i18n.confirm_clear)) {
|
||
return;
|
||
}
|
||
|
||
var $button = $('#clear-logs');
|
||
var originalText = $button.text();
|
||
|
||
$button.text(yoone_logs_params.i18n.clearing).prop('disabled', true);
|
||
|
||
$.ajax({
|
||
url: yoone_logs_params.ajax_url,
|
||
type: 'POST',
|
||
data: {
|
||
action: 'yoone_clear_logs',
|
||
nonce: yoone_logs_params.nonce
|
||
},
|
||
success: function(response) {
|
||
if (response.success) {
|
||
location.reload();
|
||
} else {
|
||
alert(response.data.message || '清空失败');
|
||
}
|
||
},
|
||
error: function() {
|
||
alert('请求失败,请重试');
|
||
},
|
||
complete: function() {
|
||
$button.text(originalText).prop('disabled', false);
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 下载日志
|
||
*/
|
||
downloadLogs: function() {
|
||
var $button = $('#download-logs');
|
||
var originalText = $button.text();
|
||
|
||
$button.text(yoone_logs_params.i18n.downloading).prop('disabled', true);
|
||
|
||
// 创建隐藏的下载链接
|
||
var downloadUrl = yoone_logs_params.ajax_url + '?action=yoone_download_logs&nonce=' + yoone_logs_params.nonce;
|
||
var $link = $('<a>').attr({
|
||
href: downloadUrl,
|
||
download: 'yoone-subscriptions-' + new Date().toISOString().split('T')[0] + '.log'
|
||
}).appendTo('body');
|
||
|
||
$link[0].click();
|
||
$link.remove();
|
||
|
||
setTimeout(function() {
|
||
$button.text(originalText).prop('disabled', false);
|
||
}, 2000);
|
||
},
|
||
|
||
/**
|
||
* 切换日志详情显示
|
||
*/
|
||
toggleLogDetails: function(e) {
|
||
var $row = $(e.currentTarget);
|
||
var $detailRow = $row.next('.log-detail-row');
|
||
|
||
if ($detailRow.length) {
|
||
$detailRow.toggle();
|
||
return;
|
||
}
|
||
|
||
// 创建详情行
|
||
var context = $row.find('.log-context').text();
|
||
if (!context) {
|
||
return;
|
||
}
|
||
|
||
var $newDetailRow = $('<tr class="log-detail-row">').html(
|
||
'<td colspan="4"><div class="log-detail">' +
|
||
'<h4>详细信息:</h4>' +
|
||
'<pre>' + this.formatJSON(context) + '</pre>' +
|
||
'</div></td>'
|
||
);
|
||
|
||
$row.after($newDetailRow);
|
||
},
|
||
|
||
/**
|
||
* 格式化JSON
|
||
*/
|
||
formatJSON: function(jsonString) {
|
||
try {
|
||
var obj = JSON.parse(jsonString);
|
||
return JSON.stringify(obj, null, 2);
|
||
} catch (e) {
|
||
return jsonString;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 执行搜索
|
||
*/
|
||
performSearch: function() {
|
||
var searchTerm = $('input[name="search"]').val();
|
||
var $rows = $('.log-table tbody tr:not(.log-detail-row)');
|
||
|
||
if (!searchTerm) {
|
||
$rows.show();
|
||
return;
|
||
}
|
||
|
||
$rows.each(function() {
|
||
var $row = $(this);
|
||
var text = $row.text().toLowerCase();
|
||
var match = text.indexOf(searchTerm.toLowerCase()) !== -1;
|
||
|
||
$row.toggle(match);
|
||
|
||
// 隐藏对应的详情行
|
||
var $detailRow = $row.next('.log-detail-row');
|
||
if ($detailRow.length) {
|
||
$detailRow.toggle(match);
|
||
}
|
||
});
|
||
},
|
||
|
||
/**
|
||
* 设置自动刷新
|
||
*/
|
||
setupAutoRefresh: function() {
|
||
// 每30秒自动刷新一次(仅在错误标签页)
|
||
if (this.getCurrentTab() === 'error') {
|
||
setInterval(function() {
|
||
if (document.visibilityState === 'visible') {
|
||
location.reload();
|
||
}
|
||
}, 30000);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 获取当前标签页
|
||
*/
|
||
getCurrentTab: function() {
|
||
var urlParams = new URLSearchParams(window.location.search);
|
||
return urlParams.get('tab') || 'recent';
|
||
},
|
||
|
||
/**
|
||
* 防抖函数
|
||
*/
|
||
debounce: function(func, wait) {
|
||
var timeout;
|
||
return function executedFunction() {
|
||
var context = this;
|
||
var args = arguments;
|
||
var later = function() {
|
||
timeout = null;
|
||
func.apply(context, args);
|
||
};
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(later, wait);
|
||
};
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 日志统计图表
|
||
*/
|
||
var LogChart = {
|
||
|
||
/**
|
||
* 初始化图表
|
||
*/
|
||
init: function() {
|
||
this.createLevelChart();
|
||
this.createTimelineChart();
|
||
},
|
||
|
||
/**
|
||
* 创建级别分布图表
|
||
*/
|
||
createLevelChart: function() {
|
||
var $container = $('#log-level-chart');
|
||
if (!$container.length) {
|
||
return;
|
||
}
|
||
|
||
// 统计各级别日志数量
|
||
var levels = {
|
||
error: 0,
|
||
warning: 0,
|
||
info: 0,
|
||
debug: 0
|
||
};
|
||
|
||
$('.log-level').each(function() {
|
||
var level = $(this).text().toLowerCase();
|
||
if (levels.hasOwnProperty(level)) {
|
||
levels[level]++;
|
||
}
|
||
});
|
||
|
||
// 创建简单的条形图
|
||
var total = Object.values(levels).reduce((a, b) => a + b, 0);
|
||
var html = '<div class="log-chart-title">日志级别分布</div>';
|
||
|
||
Object.keys(levels).forEach(function(level) {
|
||
var count = levels[level];
|
||
var percentage = total > 0 ? (count / total * 100).toFixed(1) : 0;
|
||
|
||
html += '<div class="chart-bar">' +
|
||
'<span class="bar-label">' + level + '</span>' +
|
||
'<div class="bar-container">' +
|
||
'<div class="bar-fill ' + level + '" style="width: ' + percentage + '%"></div>' +
|
||
'</div>' +
|
||
'<span class="bar-value">' + count + ' (' + percentage + '%)</span>' +
|
||
'</div>';
|
||
});
|
||
|
||
$container.html(html);
|
||
},
|
||
|
||
/**
|
||
* 创建时间线图表
|
||
*/
|
||
createTimelineChart: function() {
|
||
var $container = $('#log-timeline-chart');
|
||
if (!$container.length) {
|
||
return;
|
||
}
|
||
|
||
// 按小时统计日志数量
|
||
var hourlyStats = {};
|
||
var now = new Date();
|
||
|
||
// 初始化最近24小时
|
||
for (var i = 23; i >= 0; i--) {
|
||
var hour = new Date(now.getTime() - i * 60 * 60 * 1000);
|
||
var key = hour.getHours().toString().padStart(2, '0') + ':00';
|
||
hourlyStats[key] = 0;
|
||
}
|
||
|
||
// 统计实际日志
|
||
$('.log-table tbody tr:not(.log-detail-row)').each(function() {
|
||
var timestamp = $(this).find('td:first').text();
|
||
var date = new Date(timestamp);
|
||
var hour = date.getHours().toString().padStart(2, '0') + ':00';
|
||
|
||
if (hourlyStats.hasOwnProperty(hour)) {
|
||
hourlyStats[hour]++;
|
||
}
|
||
});
|
||
|
||
// 创建时间线图表
|
||
var maxCount = Math.max(...Object.values(hourlyStats));
|
||
var html = '<div class="log-chart-title">24小时日志趋势</div><div class="timeline-chart">';
|
||
|
||
Object.keys(hourlyStats).forEach(function(hour) {
|
||
var count = hourlyStats[hour];
|
||
var height = maxCount > 0 ? (count / maxCount * 100) : 0;
|
||
|
||
html += '<div class="timeline-bar" title="' + hour + ': ' + count + ' 条日志">' +
|
||
'<div class="timeline-fill" style="height: ' + height + '%"></div>' +
|
||
'<span class="timeline-label">' + hour + '</span>' +
|
||
'</div>';
|
||
});
|
||
|
||
html += '</div>';
|
||
$container.html(html);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 日志导出功能
|
||
*/
|
||
var LogExporter = {
|
||
|
||
/**
|
||
* 导出为CSV
|
||
*/
|
||
exportCSV: function() {
|
||
var csv = 'Timestamp,Level,Message,Context\n';
|
||
|
||
$('.log-table tbody tr:not(.log-detail-row)').each(function() {
|
||
var $row = $(this);
|
||
var cells = $row.find('td').map(function() {
|
||
return '"' + $(this).text().replace(/"/g, '""') + '"';
|
||
}).get();
|
||
|
||
csv += cells.join(',') + '\n';
|
||
});
|
||
|
||
this.downloadFile(csv, 'yoone-logs-' + new Date().toISOString().split('T')[0] + '.csv', 'text/csv');
|
||
},
|
||
|
||
/**
|
||
* 导出为JSON
|
||
*/
|
||
exportJSON: function() {
|
||
var logs = [];
|
||
|
||
$('.log-table tbody tr:not(.log-detail-row)').each(function() {
|
||
var $row = $(this);
|
||
var $cells = $row.find('td');
|
||
|
||
logs.push({
|
||
timestamp: $cells.eq(0).text(),
|
||
level: $cells.eq(1).text(),
|
||
message: $cells.eq(2).text(),
|
||
context: $cells.eq(3).text()
|
||
});
|
||
});
|
||
|
||
var json = JSON.stringify(logs, null, 2);
|
||
this.downloadFile(json, 'yoone-logs-' + new Date().toISOString().split('T')[0] + '.json', 'application/json');
|
||
},
|
||
|
||
/**
|
||
* 下载文件
|
||
*/
|
||
downloadFile: function(content, filename, contentType) {
|
||
var blob = new Blob([content], { type: contentType });
|
||
var url = window.URL.createObjectURL(blob);
|
||
var $link = $('<a>').attr({
|
||
href: url,
|
||
download: filename
|
||
}).appendTo('body');
|
||
|
||
$link[0].click();
|
||
$link.remove();
|
||
window.URL.revokeObjectURL(url);
|
||
}
|
||
};
|
||
|
||
// 文档就绪时初始化
|
||
$(document).ready(function() {
|
||
LogManager.init();
|
||
LogChart.init();
|
||
|
||
// 添加导出按钮事件
|
||
$(document).on('click', '#export-csv', LogExporter.exportCSV.bind(LogExporter));
|
||
$(document).on('click', '#export-json', LogExporter.exportJSON.bind(LogExporter));
|
||
});
|
||
|
||
// 添加样式
|
||
$('<style>').text(`
|
||
.log-detail {
|
||
background: #f9f9f9;
|
||
padding: 15px;
|
||
border-radius: 3px;
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.log-detail h4 {
|
||
margin: 0 0 10px 0;
|
||
color: #333;
|
||
}
|
||
|
||
.log-detail pre {
|
||
background: #fff;
|
||
padding: 10px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 3px;
|
||
overflow-x: auto;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.log-table tbody tr:hover {
|
||
background-color: #f5f5f5;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.log-chart-title {
|
||
font-weight: bold;
|
||
margin-bottom: 15px;
|
||
color: #333;
|
||
}
|
||
|
||
.chart-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.bar-label {
|
||
width: 80px;
|
||
font-size: 12px;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.bar-container {
|
||
flex: 1;
|
||
height: 20px;
|
||
background: #f0f0f0;
|
||
border-radius: 10px;
|
||
margin: 0 10px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.bar-fill {
|
||
height: 100%;
|
||
border-radius: 10px;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.bar-fill.error { background: #dc3545; }
|
||
.bar-fill.warning { background: #ffc107; }
|
||
.bar-fill.info { background: #17a2b8; }
|
||
.bar-fill.debug { background: #6c757d; }
|
||
|
||
.bar-value {
|
||
font-size: 12px;
|
||
color: #666;
|
||
min-width: 80px;
|
||
}
|
||
|
||
.timeline-chart {
|
||
display: flex;
|
||
align-items: end;
|
||
height: 100px;
|
||
gap: 2px;
|
||
padding: 10px 0;
|
||
}
|
||
|
||
.timeline-bar {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
height: 100%;
|
||
}
|
||
|
||
.timeline-fill {
|
||
width: 100%;
|
||
background: #007cba;
|
||
border-radius: 2px 2px 0 0;
|
||
transition: height 0.3s ease;
|
||
min-height: 2px;
|
||
}
|
||
|
||
.timeline-label {
|
||
font-size: 10px;
|
||
color: #666;
|
||
margin-top: 5px;
|
||
transform: rotate(-45deg);
|
||
}
|
||
`).appendTo('head');
|
||
|
||
})(jQuery); |