Files
DESKTOP-72TV0V4\caoxiaozhu bda8f13446 1. 增加了请求框架
2. 增加了删除虚拟环境的脚本
2026-01-12 14:20:44 +08:00

697 lines
31 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>