Files
ai_baijiahao/templates/queue.html

1509 lines
52 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>任务队列 - 百家号管理系统</title>
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="/static/css/icons-local.css">
<link rel="stylesheet" href="/static/css/style.css">
<style>
/* 队列页面特定样式 */
.queue-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 15px;
margin-bottom: 24px;
}
.stat-card {
background: white;
padding: 16px 20px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
position: relative;
overflow: hidden;
transition: all 0.3s ease;
border-left: 3px solid transparent;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
}
.stat-card.total {
border-left-color: var(--primary-color);
}
.stat-card.pending {
border-left-color: #ED7B2F;
}
.stat-card.processing {
border-left-color: #0084FF;
}
.stat-card.completed {
border-left-color: #00A870;
}
.stat-card.failed {
border-left-color: #E34D59;
}
.stat-card.paused {
border-left-color: #9C27B0;
}
.stat-number {
font-size: 28px;
font-weight: 700;
color: var(--primary-color);
margin: 8px 0 4px;
line-height: 1;
}
.stat-label {
font-size: 13px;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 4px;
}
.stat-label i {
font-size: 14px;
}
.task-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.task-item {
background: white;
border-radius: 10px;
padding: 16px 18px;
box-shadow: 0 2px 6px rgba(0,0,0,0.06);
transition: all 0.3s ease;
border-left: 3px solid #e0e0e0;
}
.task-item:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transform: translateX(2px);
}
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.task-url {
font-size: 13px;
color: #555;
word-break: break-all;
flex: 1;
margin-right: 15px;
line-height: 1.5;
display: flex;
align-items: center;
gap: 6px;
}
.task-url i {
color: var(--primary-color);
font-size: 14px;
flex-shrink: 0;
}
.task-status {
padding: 4px 14px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
flex-shrink: 0;
}
.status-pending {
background: #FFF4E6;
color: #D97706;
}
.status-processing {
background: #E0F2FE;
color: #0369A1;
}
.status-completed {
background: #D1FAE5;
color: #059669;
}
.status-failed {
background: #FEE2E2;
color: #DC2626;
}
.status-paused {
background: #F3E8FF;
color: #9333EA;
}
.task-progress {
margin: 10px 0;
}
.progress-bar-container {
background: #F3F4F6;
height: 6px;
border-radius: 3px;
overflow: hidden;
margin-bottom: 6px;
}
.progress-bar {
height: 100%;
background: var(--primary-gradient);
transition: width 0.3s ease;
border-radius: 3px;
}
.progress-text {
font-size: 12px;
color: #6B7280;
display: flex;
justify-content: space-between;
}
.task-info {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin: 10px 0;
font-size: 12px;
color: #6B7280;
}
.task-info-item {
display: flex;
align-items: center;
gap: 4px;
}
.task-info-item i {
color: var(--primary-color);
font-size: 13px;
}
.task-actions {
display: flex;
gap: 8px;
margin-top: 10px;
justify-content: flex-end;
}
.btn-action {
padding: 5px 14px;
border-radius: 6px;
border: 1px solid;
background: white;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 4px;
}
.btn-action i {
font-size: 13px;
}
.btn-view {
border-color: var(--primary-color);
color: var(--primary-color);
}
.btn-view:hover {
background: var(--primary-color);
color: white;
}
.btn-logs {
border-color: #0084FF;
color: #0084FF;
}
.btn-logs:hover {
background: #0084FF;
color: white;
}
.btn-delete {
border-color: #E34D59;
color: #E34D59;
}
.btn-delete:hover {
background: #E34D59;
color: white;
}
/* 筛选标签 */
.filter-tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.filter-tab {
padding: 6px 16px;
border-radius: 8px;
border: 1.5px solid #e0e0e0;
background: white;
color: #6B7280;
cursor: pointer;
transition: all 0.2s ease;
font-size: 13px;
font-weight: 500;
}
.filter-tab:hover {
border-color: var(--primary-color);
color: var(--primary-color);
background: #F0F7FF;
}
.filter-tab.active {
background: var(--primary-gradient);
color: white;
border-color: transparent;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 50px 20px;
color: #9CA3AF;
}
.empty-state i {
font-size: 56px;
margin-bottom: 16px;
opacity: 0.4;
}
.empty-state h3 {
font-size: 16px;
margin-bottom: 8px;
color: #6B7280;
}
.empty-state p {
font-size: 13px;
}
/* 加载动画 */
.loading-container {
display: none;
text-align: center;
padding: 50px 20px;
}
.loading-container.show {
display: block;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #F3F4F6;
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
color: var(--text-secondary);
font-size: 13px;
}
/* 分页样式 */
.pagination-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding: 14px 18px;
background: white;
border-radius: 10px;
box-shadow: 0 2px 6px rgba(0,0,0,0.06);
flex-wrap: wrap;
gap: 12px;
}
.pagination-info {
color: var(--text-secondary);
font-size: 13px;
}
.pagination-info span {
color: var(--primary-color);
font-weight: 600;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 6px;
}
.pagination-btn {
width: 32px;
height: 32px;
border: 1px solid #e0e0e0;
background: white;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
color: var(--text-secondary);
}
.pagination-btn:hover:not(:disabled) {
border-color: var(--primary-color);
color: var(--primary-color);
background: #F0F7FF;
}
.pagination-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.pagination-numbers {
display: flex;
gap: 4px;
}
.page-number {
min-width: 32px;
height: 32px;
padding: 0 10px;
border: 1px solid #e0e0e0;
background: white;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
color: var(--text-secondary);
font-size: 13px;
}
.page-number:hover:not(.active) {
border-color: var(--primary-color);
color: var(--primary-color);
background: #F0F7FF;
}
.page-number.active {
background: var(--primary-gradient);
color: white;
border-color: transparent;
font-weight: 600;
}
.page-ellipsis {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
}
.pagination-size {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-secondary);
font-size: 13px;
}
.page-size-select {
padding: 5px 10px;
border: 1px solid #e0e0e0;
border-radius: 6px;
background: white;
cursor: pointer;
font-size: 13px;
transition: all 0.2s ease;
}
.page-size-select:hover {
border-color: var(--primary-color);
}
.page-size-select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(0, 82, 217, 0.1);
}
/* 日志弹窗样式 */
.log-modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
animation: fadeIn 0.3s ease;
}
.log-modal.show {
display: flex;
align-items: center;
justify-content: center;
}
.log-modal-content {
background: white;
border-radius: 12px;
width: 90%;
max-width: 1000px;
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
animation: slideUp 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
transform: translateY(30px) scale(0.95);
opacity: 0;
}
to {
transform: translateY(0) scale(1);
opacity: 1;
}
}
.log-modal-header {
padding: 20px 24px;
border-bottom: 2px solid #E5E7EB;
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(to right, #F9FAFB, #FFFFFF);
}
.log-modal-title {
font-size: 17px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 10px;
}
.log-modal-title i {
color: var(--primary-color);
font-size: 20px;
}
.log-modal-close {
width: 34px;
height: 34px;
border: none;
background: #F3F4F6;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
color: #6B7280;
font-size: 20px;
}
.log-modal-close:hover {
background: #E5E7EB;
color: #374151;
transform: rotate(90deg);
}
.log-modal-body {
padding: 0;
overflow-y: auto;
flex: 1;
background: #F9FAFB;
}
.log-list {
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 8px;
}
.log-item {
display: grid;
grid-template-columns: auto 1fr;
gap: 12px;
padding: 10px 14px;
background: white;
border-radius: 8px;
border-left: 4px solid #E5E7EB;
font-size: 13px;
line-height: 1.6;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.log-item:hover {
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
transform: translateX(2px);
}
.log-item.log-info {
border-left-color: #0084FF;
}
.log-item.log-info .log-timestamp {
color: #0084FF;
}
.log-item.log-success {
border-left-color: #00A870;
background: #F0FDF4;
}
.log-item.log-success .log-timestamp {
color: #00A870;
}
.log-item.log-warning {
border-left-color: #F59E0B;
background: #FFFBEB;
border-left-width: 4px;
}
.log-item.log-warning .log-timestamp {
color: #D97706;
background: #FEF3C7;
font-weight: 600;
}
.log-item.log-warning .log-message {
color: #92400E;
}
.log-item.log-error {
border-left-color: #EF4444;
background: #FEF2F2;
border-left-width: 5px;
}
.log-item.log-error .log-timestamp {
color: #DC2626;
background: #FEE2E2;
font-weight: 700;
}
.log-item.log-error .log-message {
color: #991B1B;
font-weight: 500;
}
.log-timestamp {
color: #6B7280;
white-space: nowrap;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
font-weight: 600;
padding: 2px 8px;
background: #F3F4F6;
border-radius: 4px;
align-self: flex-start;
}
.log-message {
flex: 1;
color: #374151;
word-break: break-word;
padding-top: 2px;
}
.log-empty {
text-align: center;
padding: 60px 20px;
color: #9CA3AF;
}
.log-empty i {
font-size: 56px;
opacity: 0.3;
margin-bottom: 16px;
display: block;
color: #D1D5DB;
}
.log-empty h3 {
font-size: 16px;
color: #6B7280;
margin-bottom: 8px;
}
.log-empty p {
font-size: 13px;
color: #9CA3AF;
}
</style>
</head>
<body>
<!-- 主布局容器 -->
<div class="app-container">
<!-- 左侧菜单栏 -->
<aside class="sidebar">
<!-- Logo区域 -->
<div class="sidebar-logo">
<div class="sidebar-logo-icon">
<i class="bi bi-cloud-download"></i>
</div>
<div class="sidebar-logo-text">
<div class="sidebar-logo-title">百家号工具</div>
<div class="sidebar-logo-subtitle">文章导出系统</div>
</div>
</div>
<!-- 菜单导航 -->
<nav class="sidebar-nav">
<ul class="nav-menu">
<li class="nav-item">
<a href="/" class="nav-link">
<i class="bi bi-download"></i>
<span>文章导出</span>
</a>
</li>
<li class="nav-item">
<a href="/queue" class="nav-link active">
<i class="bi bi-list-task"></i>
<span>任务队列</span>
</a>
</li>
</ul>
</nav>
<!-- 用户信息区域 -->
<div class="sidebar-user">
<div class="user-info-card">
<div class="user-avatar">
<i class="bi bi-person-fill"></i>
</div>
<div class="user-details">
<div class="user-name">{{ username }}</div>
<div class="user-role">管理员</div>
</div>
<button class="logout-btn" id="logoutBtn" title="登出">
<i class="bi bi-box-arrow-right"></i>
</button>
</div>
</div>
</aside>
<!-- 主内容区域 -->
<main class="main-content">
<!-- 顶部导航栏 -->
<header class="top-navbar">
<h1 class="navbar-title">
<i class="bi bi-list-check"></i>
任务队列
</h1>
</header>
<!-- 内容区域 -->
<div class="content-area">
<!-- 页面头部 -->
<div class="page-header">
<h2 class="page-title">
<i class="bi bi-kanban"></i>
任务管理
</h2>
<p class="page-description">查看和管理所有导出任务,支持离线处理</p>
</div>
<!-- 统计卡片 -->
<div class="queue-stats">
<div class="stat-card total">
<div class="stat-label">
<i class="bi bi-collection"></i> 总任务数
</div>
<div class="stat-number" id="totalCount">0</div>
</div>
<div class="stat-card pending">
<div class="stat-label">
<i class="bi bi-hourglass-split"></i> 等待中
</div>
<div class="stat-number" id="pendingCount">0</div>
</div>
<div class="stat-card processing">
<div class="stat-label">
<i class="bi bi-arrow-repeat"></i> 处理中
</div>
<div class="stat-number" id="processingCount">0</div>
</div>
<div class="stat-card completed">
<div class="stat-label">
<i class="bi bi-check-circle"></i> 已完成
</div>
<div class="stat-number" id="completedCount">0</div>
</div>
<div class="stat-card failed">
<div class="stat-label">
<i class="bi bi-x-circle"></i> 失败
</div>
<div class="stat-number" id="failedCount">0</div>
</div>
<div class="stat-card paused">
<div class="stat-label">
<i class="bi bi-pause-circle"></i> 暂停
</div>
<div class="stat-number" id="pausedCount">0</div>
</div>
</div>
<!-- 筛选标签 -->
<div class="filter-tabs">
<button class="filter-tab active" data-filter="all">
<i class="bi bi-list"></i> 全部
</button>
<button class="filter-tab" data-filter="pending">
<i class="bi bi-hourglass-split"></i> 等待中
</button>
<button class="filter-tab" data-filter="processing">
<i class="bi bi-arrow-repeat"></i> 处理中
</button>
<button class="filter-tab" data-filter="completed">
<i class="bi bi-check-circle"></i> 已完成
</button>
<button class="filter-tab" data-filter="failed">
<i class="bi bi-x-circle"></i> 失败
</button>
<button class="filter-tab" data-filter="paused">
<i class="bi bi-pause-circle"></i> 暂停
</button>
</div>
<!-- 任务列表 -->
<div id="taskList" class="task-list">
<!-- 任务项将通过JS动态加载 -->
</div>
<!-- 分页组件 -->
<div id="paginationContainer" class="pagination-container" style="display: none;">
<div class="pagination-info">
显示 <span id="pageStart">0</span> - <span id="pageEnd">0</span> 条,共 <span id="pageTotal">0</span>
</div>
<div class="pagination-controls">
<button class="pagination-btn" id="firstPage" title="首页">
<i class="bi bi-chevron-bar-left"></i>
</button>
<button class="pagination-btn" id="prevPage" title="上一页">
<i class="bi bi-chevron-left"></i>
</button>
<div class="pagination-numbers" id="pageNumbers"></div>
<button class="pagination-btn" id="nextPage" title="下一页">
<i class="bi bi-chevron-right"></i>
</button>
<button class="pagination-btn" id="lastPage" title="尾页">
<i class="bi bi-chevron-bar-right"></i>
</button>
</div>
<div class="pagination-size">
每页显示
<select id="pageSize" class="page-size-select">
<option value="5" selected>5 条</option>
<option value="10">10 条</option>
<option value="20">20 条</option>
<option value="50">50 条</option>
</select>
</div>
</div>
<!-- 空状态 -->
<div id="emptyState" class="empty-state" style="display: none;">
<i class="bi bi-inbox"></i>
<h3>暂无任务</h3>
<p>点击“返回首页”添加新的导出任务</p>
</div>
<!-- 加载动画 -->
<div id="loadingContainer" class="loading-container">
<div class="loading-spinner"></div>
<div class="loading-text">加载中...</div>
</div>
</div>
<!-- 日志弹窗 -->
<div id="logModal" class="log-modal">
<div class="log-modal-content">
<div class="log-modal-header">
<div class="log-modal-title">
<i class="bi bi-file-text"></i>
<span id="logModalTaskId">任务日志</span>
</div>
<button class="log-modal-close" id="closeLogModal">
<i class="bi bi-x"></i>
</button>
</div>
<div class="log-modal-body">
<div id="logList" class="log-list">
<!-- 日志内容将通过JS加载 -->
</div>
</div>
</div>
</div>
</div>
<!-- jQuery - 使用国内CDN加速 -->
<!-- <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
<script src="/static/js/jquery.min.js"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script> -->
<script>
// 检查jQuery是否加载
if (typeof jQuery === 'undefined') {
console.error('jQuery未加载请检查网络连接');
alert('jQuery加载失败请刷新页面或检查网络连接');
} else {
$(document).ready(function() {
let currentFilter = 'all';
let refreshInterval = null;
let currentPage = 1;
let pageSize = 5;
let allTasks = []; // 存储所有任务
// 登出按钮
$('#logoutBtn').click(function() {
if (confirm('确定要登出吗?')) {
$.ajax({
url: '/api/logout',
type: 'POST',
success: function(response) {
if (response.success) {
window.location.href = '/login';
}
},
error: function() {
window.location.href = '/login';
}
});
}
});
// 筛选标签点击事件
$('.filter-tab').click(function() {
$('.filter-tab').removeClass('active');
$(this).addClass('active');
currentFilter = $(this).data('filter');
loadTasks();
});
// 加载任务列表
function loadTasks() {
// 保存当前滚动位置
const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
// 只在首次加载时显示加载动画
if (!$('#taskList').children().length) {
$('#loadingContainer').addClass('show');
$('#taskList').hide();
$('#emptyState').hide();
}
$.ajax({
url: '/api/queue/tasks',
type: 'GET',
success: function(response) {
// 隐藏加载动画
$('#loadingContainer').removeClass('show');
$('#taskList').show();
if (response.success) {
renderTasks(response.tasks);
// 恢复滚动位置
setTimeout(function() {
window.scrollTo(0, scrollPosition);
}, 0);
} else {
showError('加载任务列表失败');
}
},
error: function(xhr) {
// 隐藏加载动画
$('#loadingContainer').removeClass('show');
$('#taskList').show();
if (xhr.status === 401) {
alert('登录已过期,请重新登录');
window.location.href = '/login';
return;
}
showError('加载任务列表失败');
}
});
// 同时加载统计信息
loadStats();
}
// 加载统计信息
function loadStats() {
$.ajax({
url: '/api/queue/stats',
type: 'GET',
success: function(response) {
if (response.success) {
const stats = response.stats;
$('#totalCount').text(stats.total);
$('#pendingCount').text(stats.pending);
$('#processingCount').text(stats.processing);
$('#completedCount').text(stats.completed);
$('#failedCount').text(stats.failed);
$('#pausedCount').text(stats.paused || 0);
}
}
});
}
// 渲染任务列表
function renderTasks(tasks) {
const $taskList = $('#taskList');
// 筛选任务
let filteredTasks = tasks;
if (currentFilter !== 'all') {
filteredTasks = tasks.filter(task => task.status === currentFilter);
}
// 存储所有任务用于分页
allTasks = filteredTasks;
if (filteredTasks.length === 0) {
$taskList.empty();
$('#emptyState').show();
$('#paginationContainer').hide();
return;
}
$('#emptyState').hide();
// 计算分页
const totalTasks = filteredTasks.length;
const totalPages = Math.ceil(totalTasks / pageSize);
// 确保当前页在有效范围内
if (currentPage > totalPages) {
currentPage = totalPages > 0 ? totalPages : 1;
}
// 计算当前页的任务范围
const startIndex = (currentPage - 1) * pageSize;
const endIndex = Math.min(startIndex + pageSize, totalTasks);
const currentPageTasks = filteredTasks.slice(startIndex, endIndex);
// 更新分页信息
updatePaginationInfo(startIndex + 1, endIndex, totalTasks);
// 渲染分页控件
renderPaginationControls(currentPage, totalPages);
// 始终显示分页组件(即使只有一页,也显示总数信息)
if (totalTasks > 0) {
$('#paginationContainer').show();
} else {
$('#paginationContainer').hide();
}
// 使用智能更新,只更新变化的任务
const existingTaskIds = new Set();
$taskList.find('.task-item').each(function() {
const taskId = $(this).data('task-id');
if (taskId) {
existingTaskIds.add(taskId);
}
});
// 获取新任务ID
const newTaskIds = new Set(filteredTasks.map(t => t.task_id));
// 删除不存在的任务
$taskList.find('.task-item').each(function() {
const taskId = $(this).data('task-id');
if (taskId && !newTaskIds.has(taskId)) {
$(this).fadeOut(300, function() { $(this).remove(); });
}
});
// 更新或添加任务(只显示当前页)
currentPageTasks.forEach(function(task, index) {
const $existingTask = $taskList.find(`[data-task-id="${task.task_id}"]`);
if ($existingTask.length > 0) {
// 更新现有任务(只更新变化的部分)
updateTaskItem($existingTask, task);
} else {
// 添加新任务
const $taskItem = createTaskItem(task);
// 按照正确的位置插入
if (index === 0) {
$taskList.prepend($taskItem.hide().fadeIn(300));
} else {
const $prevTask = $taskList.find('.task-item').eq(index - 1);
if ($prevTask.length > 0) {
$prevTask.after($taskItem.hide().fadeIn(300));
} else {
$taskList.append($taskItem.hide().fadeIn(300));
}
}
}
});
}
// 更新任务项(只更新变化的内容)
function updateTaskItem($taskItem, task) {
// 更新状态
const statusClass = 'status-' + task.status;
const statusText = {
'pending': '等待中',
'processing': '处理中',
'completed': '已完成',
'failed': '失败',
'paused': '暂停中'
}[task.status] || task.status;
const $status = $taskItem.find('.task-status');
if ($status.text() !== statusText) {
$status.attr('class', 'task-status ' + statusClass).text(statusText);
}
// 更新进度
const progress = task.progress || 0;
const $progressBar = $taskItem.find('.progress-bar');
if ($progressBar.length > 0) {
$progressBar.css('width', progress + '%');
$taskItem.find('.progress-text span:last').text(progress + '%');
const currentStep = task.current_step || '等待处理';
$taskItem.find('.progress-text span:first').text(currentStep);
}
// 更新文章数
if (task.total_articles > 0) {
const $articlesInfo = $taskItem.find('.task-info-item:contains("文章数")');
if ($articlesInfo.length > 0) {
$articlesInfo.html(`
<i class="bi bi-file-text"></i>
文章数: ${task.total_articles}篇
`);
}
}
// 更新操作按钮
updateTaskActions($taskItem, task);
}
// 更新任务操作按钮
function updateTaskActions($taskItem, task) {
let actionsHtml = '';
if (task.status === 'completed' && task.result_file) {
actionsHtml = `
<button class="btn-action btn-view" onclick="downloadResult('${task.task_id}')" title="下载Excel">
<i class="bi bi-download"></i> 下载
</button>
<button class="btn-action btn-logs" onclick="viewLogs('${task.task_id}')" title="查看日志">
<i class="bi bi-file-text"></i> 日志
</button>
<button class="btn-action btn-delete" onclick="deleteTask('${task.task_id}')" title="删除任务">
<i class="bi bi-trash"></i> 删除
</button>
`;
} else if (task.status === 'pending' || task.status === 'processing') {
actionsHtml = `
<button class="btn-action btn-logs" onclick="viewLogs('${task.task_id}')" title="查看日志">
<i class="bi bi-file-text"></i> 日志
</button>
<button class="btn-action btn-delete" onclick="deleteTask('${task.task_id}')" title="终止并删除任务">
<i class="bi bi-trash"></i> 删除
</button>
`;
} else if (task.status === 'failed' || task.status === 'paused') {
actionsHtml = `
<button class="btn-action btn-logs" onclick="viewLogs('${task.task_id}')" title="查看日志">
<i class="bi bi-file-text"></i> 日志
</button>
<button class="btn-action btn-delete" onclick="deleteTask('${task.task_id}')" title="删除任务">
<i class="bi bi-trash"></i> 删除
</button>
`;
}
const $actions = $taskItem.find('.task-actions');
if ($actions.html() !== actionsHtml) {
$actions.html(actionsHtml);
}
}
// 创建任务项
function createTaskItem(task) {
const statusClass = 'status-' + task.status;
const statusText = {
'pending': '等待中',
'processing': '处理中',
'completed': '已完成',
'failed': '失败',
'paused': '暂停中'
}[task.status] || task.status;
const progress = task.progress || 0;
const currentStep = task.current_step || '等待处理';
let actionsHtml = '';
if (task.status === 'completed' && task.result_file) {
actionsHtml = `
<button class="btn-action btn-view" onclick="downloadResult('${task.task_id}')" title="下载Excel">
<i class="bi bi-download"></i> 下载
</button>
<button class="btn-action btn-logs" onclick="viewLogs('${task.task_id}')" title="查看日志">
<i class="bi bi-file-text"></i> 日志
</button>
<button class="btn-action btn-delete" onclick="deleteTask('${task.task_id}')" title="删除任务">
<i class="bi bi-trash"></i> 删除
</button>
`;
} else if (task.status === 'pending' || task.status === 'processing') {
actionsHtml = `
<button class="btn-action btn-logs" onclick="viewLogs('${task.task_id}')" title="查看日志">
<i class="bi bi-file-text"></i> 日志
</button>
<button class="btn-action btn-delete" onclick="deleteTask('${task.task_id}')" title="终止并删除任务">
<i class="bi bi-trash"></i> 删除
</button>
`;
} else if (task.status === 'failed' || task.status === 'paused') {
actionsHtml = `
<button class="btn-action btn-logs" onclick="viewLogs('${task.task_id}')" title="查看日志">
<i class="bi bi-file-text"></i> 日志
</button>
<button class="btn-action btn-delete" onclick="deleteTask('${task.task_id}')" title="删除任务">
<i class="bi bi-trash"></i> 删除
</button>
`;
}
const months = task.months || 6;
const monthsText = months < 1 ? `${Math.round(months * 30)}` : `${months}个月`;
return $(`
<div class="task-item" data-task-id="${task.task_id}">
<div class="task-header">
<div class="task-url">
<i class="bi bi-link-45deg"></i>
${escapeHtml(task.url)}
</div>
<span class="task-status ${statusClass}">${statusText}</span>
</div>
${task.status === 'processing' || (task.status === 'completed' && progress > 0) ? `
<div class="task-progress">
<div class="progress-bar-container">
<div class="progress-bar" style="width: ${progress}%"></div>
</div>
<div class="progress-text">
<span>${escapeHtml(currentStep)}</span>
<span>${progress}%</span>
</div>
</div>
` : ''}
<div class="task-info">
<div class="task-info-item">
<i class="bi bi-calendar-range"></i>
时间范围: 近${monthsText}
</div>
<div class="task-info-item">
<i class="bi bi-clock"></i>
创建时间: ${task.created_at}
</div>
${task.total_articles > 0 ? `
<div class="task-info-item">
<i class="bi bi-file-text"></i>
文章数: ${task.total_articles}篇
</div>
` : ''}
${task.use_proxy ? `
<div class="task-info-item">
<i class="bi bi-shield-check"></i>
使用代理
</div>
` : ''}
</div>
<div class="task-actions">
${actionsHtml}
</div>
</div>
`);
}
// 下载结果
window.downloadResult = function(taskId) {
window.location.href = `/api/queue/download/${taskId}`;
};
// 查看日志
window.viewLogs = function(taskId) {
// 显示弹窗
$('#logModal').addClass('show');
$('#logModalTaskId').text(`任务日志 - ${taskId}`);
// 加载日志
const $logList = $('#logList');
$logList.html('<div class="log-empty"><i class="bi bi-hourglass-split"></i><p>加载中...</p></div>');
$.ajax({
url: `/api/queue/task/${taskId}/logs`,
type: 'GET',
success: function(response) {
if (response.success && response.logs && response.logs.length > 0) {
let logsHtml = '';
response.logs.forEach(function(log) {
const levelClass = `log-${log.level || 'info'}`;
logsHtml += `
<div class="log-item ${levelClass}">
<div class="log-timestamp">${log.timestamp}</div>
<div class="log-message">${escapeHtml(log.message)}</div>
</div>
`;
});
$logList.html(logsHtml);
} else {
$logList.html('<div class="log-empty"><i class="bi bi-inbox"></i><p>暂无日志</p></div>');
}
},
error: function(xhr) {
if (xhr.status === 401) {
alert('登录已过期,请重新登录');
window.location.href = '/login';
return;
}
$logList.html('<div class="log-empty"><i class="bi bi-exclamation-triangle"></i><p>加载失败</p></div>');
}
});
};
// 关闭日志弹窗
$('#closeLogModal').click(function() {
$('#logModal').removeClass('show');
});
// 点击弹窗背景关闭
$('#logModal').click(function(e) {
if (e.target === this) {
$(this).removeClass('show');
}
});
// 删除任务
window.deleteTask = function(taskId) {
if (!confirm('确定要删除这个任务吗?如果任务正在运行将被终止。')) {
return;
}
$.ajax({
url: `/api/queue/task/${taskId}/delete`,
type: 'POST',
success: function(response) {
if (response.success) {
loadTasks();
} else {
alert('删除失败: ' + (response.message || '未知错误'));
}
},
error: function() {
alert('删除失败,请稍后重试');
}
});
};
// 更新分页信息显示
function updatePaginationInfo(start, end, total) {
$('#pageStart').text(start);
$('#pageEnd').text(end);
$('#pageTotal').text(total);
}
// 渲染分页控件
function renderPaginationControls(current, total) {
// 当只有一页或没有页面时,禁用所有按钮
const isDisabled = total <= 1;
// 更新按钮状态
$('#firstPage').prop('disabled', isDisabled || current === 1);
$('#prevPage').prop('disabled', isDisabled || current === 1);
$('#nextPage').prop('disabled', isDisabled || current === total);
$('#lastPage').prop('disabled', isDisabled || current === total);
// 生成页码
const $pageNumbers = $('#pageNumbers');
$pageNumbers.empty();
if (total === 0) {
// 没有数据,不显示页码
return;
}
if (total <= 7) {
// 页数少于等于7全部显示
for (let i = 1; i <= total; i++) {
$pageNumbers.append(createPageNumber(i, current));
}
} else {
// 页数多于7智能显示
// 始终显示第一页
$pageNumbers.append(createPageNumber(1, current));
if (current > 3) {
// 显示省略号
$pageNumbers.append('<span class="page-ellipsis">...</span>');
}
// 显示当前页前后各2页
const start = Math.max(2, current - 2);
const end = Math.min(total - 1, current + 2);
for (let i = start; i <= end; i++) {
$pageNumbers.append(createPageNumber(i, current));
}
if (current < total - 2) {
// 显示省略号
$pageNumbers.append('<span class="page-ellipsis">...</span>');
}
// 始终显示最后一页
$pageNumbers.append(createPageNumber(total, current));
}
}
// 创建页码按钮
function createPageNumber(pageNum, currentPageNum) {
const $pageBtn = $(`<button class="page-number ${pageNum === currentPageNum ? 'active' : ''}">${pageNum}</button>`);
$pageBtn.click(function() {
if (pageNum !== currentPage) {
currentPage = pageNum;
loadTasks();
}
});
return $pageBtn;
}
// 分页按钮事件
$('#firstPage').click(function() {
if (currentPage !== 1) {
currentPage = 1;
loadTasks();
}
});
$('#prevPage').click(function() {
if (currentPage > 1) {
currentPage--;
loadTasks();
}
});
$('#nextPage').click(function() {
const totalPages = Math.ceil(allTasks.length / pageSize);
if (currentPage < totalPages) {
currentPage++;
loadTasks();
}
});
$('#lastPage').click(function() {
const totalPages = Math.ceil(allTasks.length / pageSize);
if (currentPage !== totalPages) {
currentPage = totalPages;
loadTasks();
}
});
// 每页条数改变事件
$('#pageSize').change(function() {
pageSize = parseInt($(this).val());
currentPage = 1; // 重置到第一页
loadTasks();
});
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 显示错误
function showError(message) {
alert(message);
}
// 初始加载
loadTasks();
// 自动刷新每10秒减少频率避免卡顿
refreshInterval = setInterval(function() {
loadTasks();
}, 10000);
// 页面卸载时清除定时器
$(window).on('beforeunload', function() {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
});
}
</script>
</div>
</main>
</div>
</body>
</html>