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