Files
YG_FT_Platform/request/static/log.html

697 lines
31 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>X-Request 日志管理</title>
<!-- 引入Tailwind CSS (离线版本) -->
<script src="vendor/tailwind.min.js"></script>
<!-- 引入内联 SVG 图标系统 (完全离线) -->
<script src="vendor/icons.js"></script>
<style>
.inline-icon, .inline-emoji {
display: inline-block;
vertical-align: middle;
}
.inline-icon svg {
width: 1em;
height: 1em;
fill: currentColor;
}
.inline-emoji {
font-size: 1em;
line-height: 1;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.inline-icon[data-spin="true"], .inline-emoji[data-spin="true"] {
animation: spin 1s linear infinite;
}
</style>
<!-- 配置Tailwind -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3b82f6',
secondary: '#8b5cf6',
success: '#10b981',
warning: '#f59e0b',
danger: '#ef4444',
},
},
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
}
/* 全屏样式 */
html, body {
height: 100vh;
margin: 0;
padding: 0;
}
body {
display: flex;
flex-direction: column;
}
main {
flex: 1;
padding: 1rem;
}
.max-w-7xl {
max-width: 100%;
}
/* 调整网格布局高度 */
.h-full {
height: 100%;
}
/* 调整滚动区域最大高度 */
.max-h-screen-content {
max-height: calc(100vh - 200px);
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<!-- 顶部导航栏 -->
<nav class="bg-white shadow-md h-16">
<div class="w-full mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16 items-center">
<div class="flex items-center">
<div class="flex-shrink-0 flex items-center">
<span class="text-3xl font-bold text-primary">X</span>
<span class="ml-2 text-xl font-semibold text-gray-800">X-Request 管理系统</span>
</div>
<div class="ml-6 flex items-center space-x-4">
<a href="/" class="px-3 py-2 rounded-md text-sm font-medium text-gray-500 hover:text-primary hover:bg-primary/10 hover:border-b-2 hover:border-primary transition-colors">
<i class="fa fa-home mr-1"></i> 首页
</a>
<a href="/log.html" class="px-3 py-2 rounded-md text-sm font-medium text-primary bg-primary/10 border-b-2 border-primary">
<i class="fa fa-file-text mr-1"></i> 日志管理
</a>
<a href="/doc.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-500 hover:text-primary hover:bg-primary/10 hover:border-b-2 hover:border-primary transition-colors">
<i class="fa fa-book mr-1"></i> 接口文档
</a>
<a href="/status.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-500 hover:text-primary hover:bg-primary/10 hover:border-b-2 hover:border-primary transition-colors">
<i class="fa fa-heartbeat mr-1"></i> 系统状态
</a>
</div>
</div>
<div class="flex items-center">
<div class="mr-4">
<span class="text-sm text-gray-500">更新时间: </span>
<span id="last-updated" class="font-medium text-gray-800">--:--:--</span>
</div>
<button type="button" id="refresh-btn" class="bg-primary hover:bg-primary/90 text-white px-3 py-1.5 rounded-md text-sm font-medium transition-colors mr-2">
<i class="fa fa-refresh mr-1"></i> 刷新
</button>
<button type="button" id="batch-download-btn" onclick="batchDownload()" class="bg-success hover:bg-success/90 text-white px-3 py-1.5 rounded-md text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed mr-2" disabled>
<i class="fa fa-download mr-1"></i> 批量下载
<span id="selected-count-download" class="ml-1 bg-white/20 px-2 py-0.5 rounded-full text-xs">0</span>
</button>
<button type="button" id="batch-delete-btn" onclick="batchDelete()" class="bg-danger hover:bg-danger/90 text-white px-3 py-1.5 rounded-md text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed" disabled>
<i class="fa fa-trash mr-1"></i> 批量删除
<span id="selected-count-delete" class="ml-1 bg-white/20 px-2 py-0.5 rounded-full text-xs">0</span>
</button>
</div>
</div>
</div>
</nav>
<!-- 主要内容 -->
<main class="w-full px-4 sm:px-6 lg:px-8 py-4">
<!-- 日志管理视图 -->
<div class="bg-white rounded-lg shadow-md p-4 h-full">
<h2 class="text-xl font-bold text-gray-800 mb-4">日志管理</h2>
<!-- 日志分类和内容 -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 h-screen-content">
<!-- 左侧:日期列表 -->
<div class="lg:col-span-2">
<div class="bg-white rounded-lg shadow-sm p-4 h-full flex flex-col">
<h3 class="text-lg font-semibold text-gray-800 mb-4">按日期分类</h3>
<div class="space-y-2 flex-1 flex flex-col">
<div class="flex items-center">
<input type="checkbox" id="select-all-dates" class="rounded text-primary focus:ring-primary h-4 w-4 mr-2">
<label for="select-all-dates" class="text-sm font-medium text-gray-700">全选日期</label>
</div>
<div id="dates-list" class="space-y-1 flex-1 overflow-y-auto mt-2">
<!-- 日期列表将通过JavaScript动态生成 -->
<div class="text-center text-gray-500 py-8">
<i class="fa fa-spinner fa-spin text-xl mb-2"></i>
<p>加载中...</p>
</div>
</div>
</div>
</div>
</div>
<!-- 中间:日志文件列表 -->
<div class="lg:col-span-3">
<div class="bg-white rounded-lg shadow-sm p-4 h-full flex flex-col">
<div class="flex justify-between items-center mb-4">
<h3 id="selected-date-title" class="text-lg font-semibold text-gray-800">选择日期查看日志</h3>
<div class="flex items-center">
<input type="checkbox" id="select-all-logs" class="rounded text-primary focus:ring-primary h-4 w-4 mr-2">
<label for="select-all-logs" class="text-sm font-medium text-gray-700">全选日志</label>
</div>
</div>
<div id="logs-list" class="space-y-2 flex-1 overflow-y-auto">
<!-- 日志文件列表将通过JavaScript动态生成 -->
<div class="text-center text-gray-500 py-8">
<p>请选择左侧日期</p>
</div>
</div>
</div>
</div>
<!-- 右侧:日志内容 -->
<div class="lg:col-span-7">
<div class="bg-white rounded-lg shadow-sm p-4 h-full flex flex-col">
<div class="flex justify-between items-center mb-4">
<h3 id="current-log-title" class="text-lg font-semibold text-gray-800">日志内容</h3>
<div class="flex items-center space-x-2">
<select id="log-mode" class="border border-gray-300 rounded-md text-sm px-2 py-1">
<option value="latest" selected>最新的N条日志顶部</option>
<option value="oldest">最早的N条日志顶部</option>
</select>
<select id="log-lines" class="border border-gray-300 rounded-md text-sm px-2 py-1">
<option value="50">50行</option>
<option value="100" selected>100行</option>
<option value="200">200行</option>
<option value="500">500行</option>
<option value="1000">1000行</option>
<option value="2000">2000行</option>
</select>
</div>
</div>
<div class="bg-gray-900 text-gray-200 rounded-md p-4 flex-1 overflow-auto">
<pre id="log-content" class="text-sm whitespace-pre-wrap">请选择日志文件查看内容...</pre>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- 自定义确认对话框 -->
<div id="confirm-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
<div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-800 mb-2" id="confirm-title">确认操作</h3>
<p class="text-gray-600 mb-6" id="confirm-message">确定要执行此操作吗?</p>
<div class="flex justify-end space-x-3">
<button type="button" id="confirm-cancel" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors">
取消
</button>
<button type="button" id="confirm-ok" class="px-4 py-2 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors">
确定
</button>
</div>
</div>
</div>
</div>
<!-- JavaScript -->
<script>
// 全局变量
let selectedDate = null;
let currentLogFile = null;
let selectedFiles = new Set(); // 存储选中的日志文件路径
let dates = []; // 存储所有日期
// 自定义确认对话框
let confirmResolver = null;
function showConfirm(title, message) {
return new Promise((resolve) => {
confirmResolver = resolve;
const modal = document.getElementById('confirm-modal');
const titleEl = document.getElementById('confirm-title');
const messageEl = document.getElementById('confirm-message');
titleEl.textContent = title;
messageEl.textContent = message;
modal.classList.remove('hidden');
modal.classList.add('flex');
});
}
// 确认对话框事件监听
document.getElementById('confirm-cancel').addEventListener('click', () => {
const modal = document.getElementById('confirm-modal');
modal.classList.add('hidden');
modal.classList.remove('flex');
if (confirmResolver) {
confirmResolver(false);
confirmResolver = null;
}
});
document.getElementById('confirm-ok').addEventListener('click', () => {
const modal = document.getElementById('confirm-modal');
modal.classList.add('hidden');
modal.classList.remove('flex');
if (confirmResolver) {
confirmResolver(true);
confirmResolver = null;
}
});
// 工具函数
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
// 获取日期列表
async function fetchDates() {
try {
const response = await fetch('/monitoring/logs');
const data = await response.json();
if (data.success) {
dates = data.data.dates || [];
renderDatesList();
return true;
}
} catch (error) {
console.error('获取日期列表失败:', error);
document.getElementById('dates-list').innerHTML = '<div class="text-center text-red-500 py-8"><i class="fa fa-exclamation-circle text-xl mb-2"></i><p>获取日期列表失败</p></div>';
}
return false;
}
// 渲染日期列表
function renderDatesList() {
const datesList = document.getElementById('dates-list');
if (dates.length === 0) {
datesList.innerHTML = '<div class="text-center text-gray-500 py-8"><p>没有找到日志日期</p></div>';
return;
}
let html = '';
for (const date of dates) {
const isSelected = selectedDate === date.date;
html += `
<div class="flex items-center p-2 rounded-md border border-gray-200 cursor-pointer hover:bg-gray-100 transition-colors ${isSelected ? 'bg-primary/10 border-l-4 border-primary' : ''}">
<input type="checkbox" data-date="${date.date}" class="date-checkbox rounded text-primary focus:ring-primary h-4 w-4 mr-2">
<div class="flex-1" onclick="selectDate('${date.date}')">
<div class="font-medium text-gray-800">${date.date}</div>
<div class="text-xs text-gray-500">${date.log_count} 个日志文件</div>
</div>
<div class="text-xs text-gray-500">${new Date(date.last_modified * 1000).toLocaleString()}</div>
</div>
`;
}
datesList.innerHTML = html;
// 添加日期复选框事件监听
document.querySelectorAll('.date-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', handleDateCheckboxChange);
});
}
// 选择日期
async function selectDate(date) {
selectedDate = date;
currentLogFile = null;
selectedFiles.clear();
updateSelectedCount();
// 更新日期列表样式
renderDatesList();
// 更新日志列表
await fetchLogsByDate(date);
// 清空日志内容
document.getElementById('current-log-title').textContent = '日志内容';
document.getElementById('log-content').textContent = '请选择日志文件查看内容...';
}
// 获取指定日期的日志文件
async function fetchLogsByDate(date) {
try {
const response = await fetch(`/monitoring/logs?date=${date}`);
const data = await response.json();
if (data.success) {
const logs = data.data.logs || [];
renderLogsList(logs, date);
return true;
}
} catch (error) {
console.error('获取日志文件列表失败:', error);
document.getElementById('logs-list').innerHTML = '<div class="text-center text-red-500 py-8"><i class="fa fa-exclamation-circle text-xl mb-2"></i><p>获取日志文件列表失败</p></div>';
}
return false;
}
// 渲染日志文件列表
function renderLogsList(logs, date) {
const logsList = document.getElementById('logs-list');
const selectedDateTitle = document.getElementById('selected-date-title');
selectedDateTitle.textContent = `${date} 日志文件 (${logs.length} 个)`;
if (logs.length === 0) {
logsList.innerHTML = '<div class="text-center text-gray-500 py-8"><p>该日期没有日志文件</p></div>';
return;
}
let html = '';
for (const log of logs) {
const isSelected = selectedFiles.has(log.relative_path);
// 将Windows风格的路径分隔符替换为Unix风格
const displayPath = log.relative_path.replace(/\\/g, '/');
html += `
<div class="flex items-center p-2 rounded-md border border-gray-200 hover:bg-gray-100 transition-colors">
<input type="checkbox" data-path="${log.relative_path}" class="log-checkbox rounded text-primary focus:ring-primary h-4 w-4 mr-2" ${isSelected ? 'checked' : ''}>
<div class="flex-1" onclick="viewLog('${displayPath}', '${log.name}')">
<div class="font-medium text-gray-800">${log.name}</div>
<div class="text-xs text-gray-500">${formatBytes(log.size)} · ${new Date(log.modified_at * 1000).toLocaleString()}</div>
</div>
<a href="/monitoring/logs/${log.relative_path}/download" target="_blank" class="text-primary hover:text-primary/80 mr-2" title="下载">
<i class="fa fa-download"></i>
</a>
<button type="button" onclick="deleteLog('${displayPath}', '${log.name}')" class="text-danger hover:text-danger/80" title="删除">
<i class="fa fa-trash"></i>
</button>
</div>
`;
}
logsList.innerHTML = html;
// 添加日志复选框事件监听
document.querySelectorAll('.log-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', handleLogCheckboxChange);
});
}
// 查看日志内容
async function viewLog(filePath, fileName) {
currentLogFile = filePath;
// 更新标题
document.getElementById('current-log-title').textContent = `日志内容 - ${fileName}`;
try {
const entries = document.getElementById('log-lines').value;
const mode = document.getElementById('log-mode').value;
// 将Windows风格的路径分隔符替换为Unix风格
const normalizedPath = filePath.replace(/\\/g, '/');
const parts = normalizedPath.split('/');
const date = parts[0];
const logName = parts.slice(1).join('/');
const response = await fetch(`/monitoring/logs/${date}/${logName}?entries=${entries}&mode=${mode}`);
const data = await response.json();
if (data.success) {
document.getElementById('log-content').textContent = data.data.content;
// 根据显示模式滚动
const logContent = document.getElementById('log-content');
logContent.scrollTop = 0;
} else {
document.getElementById('log-content').textContent = `获取日志内容失败: ${data.message}`;
}
} catch (error) {
console.error('获取日志内容失败:', error);
document.getElementById('log-content').textContent = `获取日志内容失败: ${error.message}`;
}
}
// 处理日期复选框变化
async function handleDateCheckboxChange(e) {
const date = e.target.dataset.date;
const isChecked = e.target.checked;
if (isChecked) {
// 选中该日期下的所有日志文件
await fetchLogsByDate(date);
// 勾选该日期下的所有日志文件
document.querySelectorAll('.log-checkbox').forEach(checkbox => {
const path = checkbox.dataset.path;
if (path && path.startsWith(date)) {
checkbox.checked = true;
selectedFiles.add(path);
}
});
} else {
// 取消选中该日期下的所有日志文件
document.querySelectorAll('.log-checkbox').forEach(checkbox => {
const path = checkbox.dataset.path;
if (path && path.startsWith(date)) {
checkbox.checked = false;
selectedFiles.delete(path);
}
});
}
updateSelectedCount();
}
// 处理日志复选框变化
function handleLogCheckboxChange(e) {
const filePath = e.target.dataset.path;
const isChecked = e.target.checked;
if (isChecked) {
selectedFiles.add(filePath);
} else {
selectedFiles.delete(filePath);
}
updateSelectedCount();
}
// 更新选中计数
function updateSelectedCount() {
const count = selectedFiles.size;
// 更新批量下载的计数
document.getElementById('selected-count-download').textContent = count;
// 更新批量删除的计数
document.getElementById('selected-count-delete').textContent = count;
// 更新按钮状态
document.getElementById('batch-download-btn').disabled = count === 0;
document.getElementById('batch-delete-btn').disabled = count === 0;
}
// 删除单个日志文件
async function deleteLog(filePath, fileName) {
const confirmed = await showConfirm('删除确认', `确定要删除日志文件 "${fileName}" 吗?此操作不可恢复。`);
if (!confirmed) {
return;
}
try {
const response = await fetch(`/monitoring/logs/${filePath}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
// 更新日志列表
await fetchLogsByDate(selectedDate);
// 清空日志内容如果当前显示的是被删除的日志
if (currentLogFile === filePath) {
document.getElementById('current-log-title').textContent = '日志内容';
document.getElementById('log-content').textContent = '请选择日志文件查看内容...';
currentLogFile = null;
}
// 更新选中文件集合
selectedFiles.delete(filePath);
updateSelectedCount();
// 显示成功提示
alert(`日志文件 "${fileName}" 删除成功`);
} else {
alert(`删除失败: ${data.message}`);
}
} catch (error) {
console.error('删除日志失败:', error);
alert(`删除失败: ${error.message}`);
}
}
// 批量删除日志文件
async function batchDelete() {
if (selectedFiles.size === 0) {
alert('请先选择日志文件');
return;
}
const confirmed = await showConfirm('批量删除确认', `确定要删除选中的 ${selectedFiles.size} 个日志文件吗?此操作不可恢复。`);
if (!confirmed) {
return;
}
try {
const response = await fetch('/monitoring/logs/batch/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
files: Array.from(selectedFiles)
})
});
const data = await response.json();
if (data.success) {
// 更新日志列表
await fetchLogsByDate(selectedDate);
// 清空当前日志内容
document.getElementById('current-log-title').textContent = '日志内容';
document.getElementById('log-content').textContent = '请选择日志文件查看内容...';
currentLogFile = null;
// 清空选中文件集合
selectedFiles.clear();
updateSelectedCount();
// 显示成功提示
alert(`批量删除完成。成功删除 ${data.data.deleted} 个文件,失败 ${data.data.failed} 个文件。`);
} else {
alert(`批量删除失败: ${data.message}`);
}
} catch (error) {
console.error('批量删除失败:', error);
alert(`批量删除失败: ${error.message}`);
}
}
// 全选/取消全选日期
function toggleSelectAllDates() {
const selectAllCheckbox = document.getElementById('select-all-dates');
const isChecked = selectAllCheckbox.checked;
document.querySelectorAll('.date-checkbox').forEach(checkbox => {
checkbox.checked = isChecked;
// 触发change事件调用handleDateCheckboxChange处理日志文件联动
checkbox.dispatchEvent(new Event('change'));
});
}
// 全选/取消全选日志
function toggleSelectAllLogs() {
const selectAllCheckbox = document.getElementById('select-all-logs');
const isChecked = selectAllCheckbox.checked;
document.querySelectorAll('.log-checkbox').forEach(checkbox => {
checkbox.checked = isChecked;
checkbox.dispatchEvent(new Event('change'));
});
}
// 批量下载日志
async function batchDownload() {
if (selectedFiles.size === 0) {
alert('请先选择日志文件');
return;
}
try {
const response = await fetch('/monitoring/logs/download', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
files: Array.from(selectedFiles)
})
});
if (response.ok) {
// 创建下载链接
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `logs_${new Date().toISOString().slice(0, 10)}_batch.zip`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} else {
const data = await response.json();
alert(`批量下载失败: ${data.message}`);
}
} catch (error) {
console.error('批量下载失败:', error);
alert(`批量下载失败: ${error.message}`);
}
}
// 刷新所有数据
async function refreshAll() {
await fetchDates();
if (selectedDate) {
await fetchLogsByDate(selectedDate);
}
// 更新最后更新时间
const now = new Date();
document.getElementById('last-updated').textContent = now.toLocaleTimeString();
}
// 初始化
async function init() {
// 初始加载数据
await refreshAll();
// 事件监听
document.getElementById('refresh-btn').addEventListener('click', refreshAll);
document.getElementById('select-all-dates').addEventListener('change', toggleSelectAllDates);
document.getElementById('select-all-logs').addEventListener('change', toggleSelectAllLogs);
// 日志行数或显示模式变化事件
document.getElementById('log-lines').addEventListener('change', () => {
if (currentLogFile) {
// 直接调用viewLog使用当前的filePath和fileName
viewLog(currentLogFile, document.getElementById('current-log-title').textContent.replace('日志内容 - ', ''));
}
});
// 显示模式变化事件
document.getElementById('log-mode').addEventListener('change', () => {
if (currentLogFile) {
// 直接调用viewLog使用当前的filePath和fileName
viewLog(currentLogFile, document.getElementById('current-log-title').textContent.replace('日志内容 - ', ''));
}
});
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>