420 lines
14 KiB
HTML
420 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>TaskQ 监控面板</title>
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
margin: 0;
|
|
padding: 20px;
|
|
background-color: #f5f5f5;
|
|
}
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
overflow: hidden;
|
|
}
|
|
.header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 20px;
|
|
text-align: center;
|
|
}
|
|
.header h1 {
|
|
margin: 0;
|
|
font-size: 2em;
|
|
}
|
|
.content {
|
|
padding: 20px;
|
|
}
|
|
.queues-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
margin-top: 20px;
|
|
}
|
|
.queue-card {
|
|
border: 1px solid #e1e5e9;
|
|
border-radius: 6px;
|
|
padding: 15px;
|
|
background: #fafafa;
|
|
cursor: pointer;
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
}
|
|
.queue-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
|
}
|
|
.queue-name {
|
|
font-weight: bold;
|
|
color: #333;
|
|
margin-bottom: 10px;
|
|
font-size: 1.1em;
|
|
}
|
|
.queue-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 10px;
|
|
}
|
|
.stat {
|
|
text-align: center;
|
|
padding: 8px;
|
|
background: white;
|
|
border-radius: 4px;
|
|
border: 1px solid #e1e5e9;
|
|
}
|
|
.stat-value {
|
|
font-size: 1.2em;
|
|
font-weight: bold;
|
|
color: #667eea;
|
|
}
|
|
.stat-label {
|
|
font-size: 0.8em;
|
|
color: #666;
|
|
margin-top: 2px;
|
|
}
|
|
.loading {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: #666;
|
|
}
|
|
.error {
|
|
background: #fee;
|
|
color: #c33;
|
|
padding: 15px;
|
|
border-radius: 4px;
|
|
margin: 20px 0;
|
|
}
|
|
.refresh-btn {
|
|
background: #667eea;
|
|
color: white;
|
|
border: none;
|
|
padding: 10px 20px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
margin-bottom: 20px;
|
|
}
|
|
.refresh-btn:hover {
|
|
background: #5a6fd8;
|
|
}
|
|
.modal {
|
|
display: none;
|
|
position: fixed;
|
|
z-index: 1000;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0,0,0,0.5);
|
|
}
|
|
.modal-content {
|
|
background-color: white;
|
|
margin: 5% auto;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
width: 80%;
|
|
max-width: 800px;
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
}
|
|
.close {
|
|
color: #aaa;
|
|
float: right;
|
|
font-size: 28px;
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
}
|
|
.close:hover {
|
|
color: #000;
|
|
}
|
|
.task-tabs {
|
|
display: flex;
|
|
border-bottom: 1px solid #e1e5e9;
|
|
margin-bottom: 20px;
|
|
}
|
|
.task-tab {
|
|
padding: 10px 20px;
|
|
cursor: pointer;
|
|
border: none;
|
|
background: none;
|
|
border-bottom: 2px solid transparent;
|
|
}
|
|
.task-tab.active {
|
|
border-bottom-color: #667eea;
|
|
color: #667eea;
|
|
font-weight: bold;
|
|
}
|
|
.task-list {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
.task-item {
|
|
border: 1px solid #e1e5e9;
|
|
border-radius: 4px;
|
|
padding: 10px;
|
|
margin-bottom: 10px;
|
|
background: #fafafa;
|
|
}
|
|
.task-id {
|
|
font-family: monospace;
|
|
font-size: 0.9em;
|
|
color: #666;
|
|
}
|
|
.task-type {
|
|
font-weight: bold;
|
|
color: #333;
|
|
}
|
|
.task-payload {
|
|
font-family: monospace;
|
|
font-size: 0.8em;
|
|
color: #666;
|
|
background: white;
|
|
padding: 5px;
|
|
border-radius: 3px;
|
|
margin-top: 5px;
|
|
word-break: break-all;
|
|
}
|
|
.pagination {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
margin-top: 20px;
|
|
}
|
|
.pagination button {
|
|
padding: 5px 10px;
|
|
border: 1px solid #e1e5e9;
|
|
background: white;
|
|
cursor: pointer;
|
|
border-radius: 3px;
|
|
}
|
|
.pagination button:hover {
|
|
background: #f0f0f0;
|
|
}
|
|
.pagination button.active {
|
|
background: #667eea;
|
|
color: white;
|
|
border-color: #667eea;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>🚀 TaskQ 监控面板</h1>
|
|
<p>实时监控异步任务队列状态</p>
|
|
</div>
|
|
<div class="content">
|
|
<button class="refresh-btn" onclick="loadQueues()">🔄 刷新</button>
|
|
<div id="loading" class="loading">加载中...</div>
|
|
<div id="error" class="error" style="display: none;"></div>
|
|
<div id="queues" class="queues-grid" style="display: none;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 任务详情模态框 -->
|
|
<div id="taskModal" class="modal">
|
|
<div class="modal-content">
|
|
<span class="close" onclick="closeTaskModal()">×</span>
|
|
<h2 id="modalTitle">队列任务详情</h2>
|
|
<div class="task-tabs">
|
|
<button class="task-tab active" onclick="switchTab('active')">活跃</button>
|
|
<button class="task-tab" onclick="switchTab('pending')">等待</button>
|
|
<button class="task-tab" onclick="switchTab('retry')">重试</button>
|
|
<button class="task-tab" onclick="switchTab('archived')">归档</button>
|
|
<button class="task-tab" onclick="switchTab('completed')">完成</button>
|
|
</div>
|
|
<div id="taskContent" class="task-list"></div>
|
|
<div id="pagination" class="pagination"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let currentQueue = '';
|
|
let currentTab = 'active';
|
|
let currentPage = 1;
|
|
const pageSize = 20;
|
|
|
|
async function loadQueues() {
|
|
const loading = document.getElementById('loading');
|
|
const error = document.getElementById('error');
|
|
const queues = document.getElementById('queues');
|
|
|
|
loading.style.display = 'block';
|
|
error.style.display = 'none';
|
|
queues.style.display = 'none';
|
|
|
|
try {
|
|
const response = await fetch('{{.RootPath}}/api/queues');
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const queueData = await response.json();
|
|
displayQueues(queueData);
|
|
|
|
} catch (err) {
|
|
error.textContent = '加载队列数据失败: ' + err.message;
|
|
error.style.display = 'block';
|
|
} finally {
|
|
loading.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function displayQueues(queueData) {
|
|
const queues = document.getElementById('queues');
|
|
|
|
if (queueData.length === 0) {
|
|
queues.innerHTML = '<div style="text-align: center; padding: 40px; color: #666;">暂无队列数据</div>';
|
|
queues.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
queues.innerHTML = queueData.map(queue => `
|
|
<div class="queue-card" onclick="openTaskModal('${queue.name}')">
|
|
<div class="queue-name">${queue.name} (优先级: ${queue.priority})</div>
|
|
<div class="queue-stats">
|
|
<div class="stat">
|
|
<div class="stat-value">${queue.active}</div>
|
|
<div class="stat-label">活跃</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value">${queue.pending}</div>
|
|
<div class="stat-label">等待</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value">${queue.retry}</div>
|
|
<div class="stat-label">重试</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value">${queue.archived}</div>
|
|
<div class="stat-label">归档</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
queues.style.display = 'grid';
|
|
}
|
|
|
|
function openTaskModal(queueName) {
|
|
currentQueue = queueName;
|
|
currentPage = 1;
|
|
document.getElementById('modalTitle').textContent = `队列 "${queueName}" 任务详情`;
|
|
document.getElementById('taskModal').style.display = 'block';
|
|
loadTasks();
|
|
}
|
|
|
|
function closeTaskModal() {
|
|
document.getElementById('taskModal').style.display = 'none';
|
|
}
|
|
|
|
function switchTab(tab) {
|
|
currentTab = tab;
|
|
currentPage = 1;
|
|
|
|
// 更新标签样式
|
|
document.querySelectorAll('.task-tab').forEach(t => t.classList.remove('active'));
|
|
event.target.classList.add('active');
|
|
|
|
loadTasks();
|
|
}
|
|
|
|
async function loadTasks() {
|
|
const taskContent = document.getElementById('taskContent');
|
|
const pagination = document.getElementById('pagination');
|
|
|
|
taskContent.innerHTML = '<div class="loading">加载任务中...</div>';
|
|
pagination.innerHTML = '';
|
|
|
|
try {
|
|
const response = await fetch(`{{.RootPath}}/api/tasks/${currentQueue}/${currentTab}?page=${currentPage}&page_size=${pageSize}`);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
displayTasks(data.tasks);
|
|
displayPagination(data.page, data.page_size, data.total);
|
|
|
|
} catch (err) {
|
|
taskContent.innerHTML = `<div class="error">加载任务失败: ${err.message}</div>`;
|
|
}
|
|
}
|
|
|
|
function displayTasks(tasks) {
|
|
const taskContent = document.getElementById('taskContent');
|
|
|
|
if (tasks.length === 0) {
|
|
taskContent.innerHTML = '<div style="text-align: center; padding: 20px; color: #666;">暂无任务</div>';
|
|
return;
|
|
}
|
|
|
|
taskContent.innerHTML = tasks.map(task => `
|
|
<div class="task-item">
|
|
<div class="task-id">ID: ${task.id}</div>
|
|
<div class="task-type">类型: ${task.type}</div>
|
|
${task.payload ? `<div class="task-payload">载荷: ${task.payload}</div>` : ''}
|
|
${task.retried > 0 ? `<div>重试次数: ${task.retried}</div>` : ''}
|
|
${task.last_error ? `<div style="color: #c33; font-size: 0.9em;">错误: ${task.last_error}</div>` : ''}
|
|
${task.next_process ? `<div style="color: #666; font-size: 0.9em;">下次处理: ${new Date(task.next_process).toLocaleString()}</div>` : ''}
|
|
${task.completed_at ? `<div style="color: #4caf50; font-size: 0.9em;">完成时间: ${new Date(task.completed_at).toLocaleString()}</div>` : ''}
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function displayPagination(page, pageSize, total) {
|
|
const pagination = document.getElementById('pagination');
|
|
|
|
if (total === 0) return;
|
|
|
|
const totalPages = Math.ceil(total / pageSize);
|
|
let paginationHTML = '';
|
|
|
|
// 上一页
|
|
if (page > 1) {
|
|
paginationHTML += `<button onclick="changePage(${page - 1})">上一页</button>`;
|
|
}
|
|
|
|
// 页码
|
|
const startPage = Math.max(1, page - 2);
|
|
const endPage = Math.min(totalPages, page + 2);
|
|
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
const activeClass = i === page ? 'active' : '';
|
|
paginationHTML += `<button class="${activeClass}" onclick="changePage(${i})">${i}</button>`;
|
|
}
|
|
|
|
// 下一页
|
|
if (page < totalPages) {
|
|
paginationHTML += `<button onclick="changePage(${page + 1})">下一页</button>`;
|
|
}
|
|
|
|
pagination.innerHTML = paginationHTML;
|
|
}
|
|
|
|
function changePage(page) {
|
|
currentPage = page;
|
|
loadTasks();
|
|
}
|
|
|
|
// 点击模态框外部关闭
|
|
window.onclick = function(event) {
|
|
const modal = document.getElementById('taskModal');
|
|
if (event.target === modal) {
|
|
closeTaskModal();
|
|
}
|
|
}
|
|
|
|
// 页面加载时自动加载数据
|
|
document.addEventListener('DOMContentLoaded', loadQueues);
|
|
|
|
// 每30秒自动刷新
|
|
setInterval(loadQueues, 30000);
|
|
</script>
|
|
</body>
|
|
</html> |