chore: 添加任务队列管理系统
This commit is contained in:
420
dashboard.html
Normal file
420
dashboard.html
Normal file
@@ -0,0 +1,420 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user