Files
taskq/dashboard.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()">&times;</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>