Files
ai_baijiahao/templates/queue.html

1509 lines
52 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>任务队列 - 百家号管理系统</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>