1007 lines
34 KiB
HTML
1007 lines
34 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>定时任务管理</title>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<!-- Bootstrap CSS -->
|
||
<link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
|
||
<!-- Bootstrap Icons -->
|
||
<link
|
||
rel="stylesheet"
|
||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css"
|
||
/>
|
||
<!-- 自定义样式 -->
|
||
<style>
|
||
/* 开关按钮样式 */
|
||
.switch {
|
||
position: relative;
|
||
display: inline-block;
|
||
width: 60px;
|
||
height: 34px;
|
||
}
|
||
.switch input {
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
}
|
||
.slider {
|
||
position: absolute;
|
||
cursor: pointer;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: #ccc;
|
||
transition: 0.4s;
|
||
border-radius: 34px;
|
||
}
|
||
.slider:before {
|
||
position: absolute;
|
||
content: "";
|
||
height: 26px;
|
||
width: 26px;
|
||
left: 4px;
|
||
bottom: 4px;
|
||
background-color: white;
|
||
transition: 0.4s;
|
||
border-radius: 50%;
|
||
}
|
||
input:checked + .slider {
|
||
background-color: #2196f3;
|
||
}
|
||
input:checked + .slider:before {
|
||
transform: translateX(26px);
|
||
}
|
||
/* 状态徽章样式 */
|
||
.status-badge {
|
||
padding: 0.5em 0.8em;
|
||
border-radius: 20px;
|
||
font-size: 0.875em;
|
||
font-weight: 600;
|
||
}
|
||
.status-running {
|
||
background-color: #0d6efd;
|
||
color: white;
|
||
}
|
||
.status-completed {
|
||
background-color: #198754;
|
||
color: white;
|
||
}
|
||
.status-failed {
|
||
background-color: #dc3545;
|
||
color: white;
|
||
}
|
||
.status-pending {
|
||
background-color: #ffc107;
|
||
color: black;
|
||
}
|
||
/* Toast容器样式 */
|
||
.toast-container {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 1060;
|
||
}
|
||
|
||
/* 表单加载状态样式 */
|
||
form.loading {
|
||
position: relative;
|
||
min-height: 200px;
|
||
}
|
||
|
||
form.loading::after {
|
||
content: "";
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(255, 255, 255, 0.7);
|
||
z-index: 1;
|
||
}
|
||
|
||
/* 编辑模态框样式 */
|
||
.modal-body {
|
||
max-height: calc(100vh - 210px);
|
||
overflow-y: auto;
|
||
}
|
||
|
||
/* 按钮加载状态样式 */
|
||
.btn:disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.65;
|
||
}
|
||
|
||
.btn .spinner-border {
|
||
margin-right: 0.5rem;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.form-control:invalid:focus {
|
||
border-color: #dc3545;
|
||
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
||
}
|
||
|
||
.form-control.is-valid {
|
||
border-color: #198754;
|
||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
|
||
background-repeat: no-repeat;
|
||
background-position: right calc(0.375em + 0.1875rem) center;
|
||
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="bg-light">
|
||
<!-- Toast通知容器 -->
|
||
<div class="toast-container"></div>
|
||
|
||
<div class="container-fluid py-4">
|
||
<div class="row g-4">
|
||
<!-- 创建定时任务表单 -->
|
||
<div class="col-md-4">
|
||
<div class="card">
|
||
<div class="card-body">
|
||
<h2 class="card-title mb-4">创建定时任务</h2>
|
||
<form id="taskForm">
|
||
<div class="mb-3">
|
||
<label for="name" class="form-label">任务名称</label>
|
||
<input
|
||
type="text"
|
||
class="form-control"
|
||
id="name"
|
||
name="name"
|
||
required
|
||
/>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="cron_expression" class="form-label"
|
||
>Cron表达式</label
|
||
>
|
||
<input
|
||
type="text"
|
||
class="form-control"
|
||
id="cron_expression"
|
||
name="cron_expression"
|
||
required
|
||
/>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="spider_name" class="form-label">爬虫名称</label>
|
||
<input
|
||
type="text"
|
||
class="form-control"
|
||
id="spider_name"
|
||
name="spider_name"
|
||
required
|
||
/>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="url" class="form-label">URL</label>
|
||
<input
|
||
type="text"
|
||
class="form-control"
|
||
id="url"
|
||
name="url"
|
||
required
|
||
/>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="video_list" class="form-label">剧集ID</label>
|
||
<input
|
||
type="number"
|
||
class="form-control"
|
||
id="video_list"
|
||
name="video_list"
|
||
required
|
||
/>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">创建任务</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 定时任务列表 -->
|
||
<div class="col-md-8">
|
||
<div class="card mb-4">
|
||
<div class="card-body">
|
||
<h2 class="card-title mb-4">定时任务列表</h2>
|
||
<div class="table-responsive">
|
||
<table id="taskList" class="table table-striped table-hover">
|
||
<thead>
|
||
<tr>
|
||
<th>任务名称</th>
|
||
<th>Cron表达式</th>
|
||
<th>爬虫名称</th>
|
||
<th>URL</th>
|
||
<th>剧集ID</th>
|
||
<th>状态</th>
|
||
<th>运行状态</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 爬虫任务列表 -->
|
||
<div class="card">
|
||
<div class="card-body">
|
||
<h2 class="card-title mb-4">爬虫任务列表</h2>
|
||
<div class="table-responsive">
|
||
<table id="spiderList" class="table table-striped table-hover">
|
||
<thead>
|
||
<tr>
|
||
<th>任务ID</th>
|
||
<th>任务名称</th>
|
||
<th>爬虫名称</th>
|
||
<th>状态</th>
|
||
<th>消息</th>
|
||
<th>开始时间</th>
|
||
<th>结束时间</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
<!-- 分页控件 -->
|
||
<nav aria-label="Page navigation">
|
||
<ul
|
||
class="pagination justify-content-center"
|
||
id="spiderPagination"
|
||
>
|
||
<li class="page-item">
|
||
<button class="page-link" id="prevPage">上一页</button>
|
||
</li>
|
||
<li class="page-item">
|
||
<span class="page-link" id="pageInfo">1/1</span>
|
||
</li>
|
||
<li class="page-item">
|
||
<button class="page-link" id="nextPage">下一页</button>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 编辑任务模态框 -->
|
||
<div class="modal fade" id="editModal" tabindex="-1">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">编辑任务</h5>
|
||
<button
|
||
type="button"
|
||
class="btn-close"
|
||
onclick="closeEditModal()"
|
||
></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="editTaskForm">
|
||
<input type="hidden" id="edit_task_id" name="task_id" />
|
||
<div class="mb-3">
|
||
<label for="edit_name" class="form-label"
|
||
>任务名称 <span class="text-danger">*</span></label
|
||
>
|
||
<input
|
||
type="text"
|
||
class="form-control"
|
||
id="edit_name"
|
||
name="name"
|
||
required
|
||
placeholder="请输入任务名称"
|
||
/>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="edit_cron_expression" class="form-label">
|
||
Cron表达式 <span class="text-danger">*</span>
|
||
<i
|
||
class="bi bi-info-circle"
|
||
data-bs-toggle="tooltip"
|
||
title="Cron表达式格式:分 时 日 月 周,例如:0 2 * * * 表示每天凌晨2点执行"
|
||
></i>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
class="form-control"
|
||
id="edit_cron_expression"
|
||
name="cron_expression"
|
||
required
|
||
placeholder="例如:0 2 * * *"
|
||
/>
|
||
<div class="form-text">使用标准cron表达式格式</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="edit_spider_name" class="form-label">
|
||
爬虫名称 <span class="text-danger">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
class="form-control"
|
||
id="edit_spider_name"
|
||
name="spider_name"
|
||
required
|
||
placeholder="请输入爬虫名称"
|
||
/>
|
||
<div class="form-text">zgjs</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="edit_url" class="form-label">
|
||
URL <span class="text-danger">*</span>
|
||
</label>
|
||
<input
|
||
type="url"
|
||
class="form-control"
|
||
id="edit_url"
|
||
name="url"
|
||
required
|
||
placeholder="请输入完整的URL地址"
|
||
/>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label for="edit_video_list" class="form-label">
|
||
剧集ID <span class="text-danger">*</span>
|
||
</label>
|
||
<input
|
||
type="number"
|
||
class="form-control"
|
||
id="edit_video_list"
|
||
name="video_list"
|
||
required
|
||
min="0"
|
||
placeholder="请输入剧集ID"
|
||
/>
|
||
</div>
|
||
<div class="modal-footer px-0 pb-0">
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary"
|
||
onclick="closeEditModal()"
|
||
>
|
||
取消
|
||
</button>
|
||
<button type="submit" class="btn btn-primary">保存修改</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bootstrap -->
|
||
<script src="/static/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||
<script>
|
||
// Toast通知函数
|
||
function showToast(message, type = "success") {
|
||
const toastContainer = document.querySelector(".toast-container");
|
||
const toastHtml = `
|
||
<div class="toast align-items-center text-white bg-${type} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||
<div class="d-flex">
|
||
<div class="toast-body">
|
||
${message}
|
||
</div>
|
||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
toastContainer.insertAdjacentHTML("beforeend", toastHtml);
|
||
const toastElement = toastContainer.lastElementChild;
|
||
const toast = new bootstrap.Toast(toastElement, {
|
||
autohide: true,
|
||
delay: 3000,
|
||
});
|
||
toast.show();
|
||
|
||
// 监听隐藏事件,移除DOM元素
|
||
toastElement.addEventListener("hidden.bs.toast", () => {
|
||
toastElement.remove();
|
||
});
|
||
}
|
||
|
||
// 编辑任务相关函数
|
||
function showEditModal() {
|
||
const modal = new bootstrap.Modal(document.getElementById("editModal"));
|
||
modal.show();
|
||
}
|
||
|
||
function closeEditModal() {
|
||
const modal = bootstrap.Modal.getInstance(
|
||
document.getElementById("editModal")
|
||
);
|
||
modal.hide();
|
||
}
|
||
|
||
async function editTask(taskId) {
|
||
try {
|
||
// 先显示模态框,并显示加载状态
|
||
showEditModal();
|
||
const form = document.getElementById("editTaskForm");
|
||
form.classList.add("loading");
|
||
|
||
// 禁用所有输入字段
|
||
const inputs = form.querySelectorAll("input, button");
|
||
inputs.forEach((input) => (input.disabled = true));
|
||
|
||
// 显示加载提示
|
||
const loadingHtml = `
|
||
<div class="text-center my-4" id="editFormLoading">
|
||
<div class="spinner-border text-primary" role="status">
|
||
<span class="visually-hidden">加载中...</span>
|
||
</div>
|
||
<p class="mt-2">正在加载任务数据...</p>
|
||
</div>
|
||
`;
|
||
form.insertAdjacentHTML("afterbegin", loadingHtml);
|
||
|
||
const response = await fetch(`/api/scheduled-tasks/${taskId}`);
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
const task = await response.json();
|
||
|
||
// 移除加载提示
|
||
const loadingElement = document.getElementById("editFormLoading");
|
||
if (loadingElement) {
|
||
loadingElement.remove();
|
||
}
|
||
|
||
// 填充表单
|
||
document.getElementById("edit_task_id").value = task.id;
|
||
document.getElementById("edit_name").value = task.name;
|
||
document.getElementById("edit_cron_expression").value =
|
||
task.cron_expression;
|
||
document.getElementById("edit_spider_name").value = task.spider_name;
|
||
document.getElementById("edit_url").value = task.url;
|
||
document.getElementById("edit_video_list").value = task.video_list;
|
||
|
||
// 启用所有输入字段
|
||
inputs.forEach((input) => (input.disabled = false));
|
||
form.classList.remove("loading");
|
||
} catch (error) {
|
||
console.error("Error:", error);
|
||
showToast(`获取任务详情失败: ${error.message}`, "danger");
|
||
closeEditModal();
|
||
}
|
||
}
|
||
|
||
// 添加编辑表单重置处理
|
||
document
|
||
.getElementById("editModal")
|
||
.addEventListener("hidden.bs.modal", function () {
|
||
const form = document.getElementById("editTaskForm");
|
||
form.reset();
|
||
// 移除可能存在的加载提示
|
||
const loadingElement = document.getElementById("editFormLoading");
|
||
if (loadingElement) {
|
||
loadingElement.remove();
|
||
}
|
||
// 确保所有输入字段都被启用
|
||
const inputs = form.querySelectorAll("input, button");
|
||
inputs.forEach((input) => (input.disabled = false));
|
||
form.classList.remove("loading");
|
||
});
|
||
|
||
// 删除任务
|
||
async function deleteTask(taskId) {
|
||
if (!confirm("确定要删除这个任务吗?")) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/scheduled-tasks/${taskId}`, {
|
||
method: "DELETE",
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast("任务已删除");
|
||
loadTasks();
|
||
} else {
|
||
const error = await response.json();
|
||
showToast(`删除失败: ${error.detail}`, "danger");
|
||
}
|
||
} catch (error) {
|
||
showToast(`删除失败: ${error.message}`, "danger");
|
||
}
|
||
}
|
||
|
||
// 更新任务状态显示
|
||
function updateTaskStatus(taskId, status) {
|
||
const statusCell = document.getElementById(`task-status-${taskId}`);
|
||
if (!statusCell) return;
|
||
|
||
let badgeClass = "";
|
||
let statusText = "";
|
||
|
||
switch (status) {
|
||
case "running":
|
||
badgeClass = "status-running";
|
||
statusText = "运行中";
|
||
break;
|
||
case "completed":
|
||
badgeClass = "status-completed";
|
||
statusText = "已完成";
|
||
break;
|
||
case "failed":
|
||
badgeClass = "status-failed";
|
||
statusText = "失败";
|
||
break;
|
||
case "pending":
|
||
badgeClass = "status-pending";
|
||
statusText = "等待中";
|
||
break;
|
||
default:
|
||
badgeClass = "";
|
||
statusText = "未运行";
|
||
}
|
||
|
||
statusCell.innerHTML = `<span class="status-badge ${badgeClass}">${statusText}</span>`;
|
||
}
|
||
|
||
// 加载定时任务列表
|
||
async function loadTasks() {
|
||
try {
|
||
const response = await fetch("/api/scheduled-tasks");
|
||
const tasks = await response.json();
|
||
const tbody = document.querySelector("#taskList tbody");
|
||
tbody.innerHTML = "";
|
||
tasks.forEach((task) => {
|
||
const row = tbody.insertRow();
|
||
row.innerHTML = `
|
||
<td>${task.name}</td>
|
||
<td>${task.cron_expression}</td>
|
||
<td>${task.spider_name}</td>
|
||
<td>${task.url}</td>
|
||
<td>${task.video_list}</td>
|
||
<td>
|
||
<label class="switch">
|
||
<input type="checkbox" ${task.enabled ? "checked" : ""}
|
||
onchange="toggleTask(${task.id}, this.checked)">
|
||
<span class="slider"></span>
|
||
</label>
|
||
</td>
|
||
<td id="task-status-${task.id}">
|
||
<span class="status-badge">未运行</span>
|
||
</td>
|
||
<td>
|
||
<button class="btn btn-primary btn-sm" onclick="runTask(${
|
||
task.id
|
||
})">
|
||
立即执行
|
||
</button>
|
||
<button class="btn btn-warning btn-sm" onclick="editTask(${
|
||
task.id
|
||
})">
|
||
编辑
|
||
</button>
|
||
<button class="btn btn-danger btn-sm" onclick="deleteTask(${
|
||
task.id
|
||
})">
|
||
删除
|
||
</button>
|
||
</td>
|
||
`;
|
||
// 检查任务状态
|
||
checkTaskStatus(task.id);
|
||
});
|
||
} catch (error) {
|
||
showToast(`加载任务列表失败: ${error.message}`, "danger");
|
||
}
|
||
}
|
||
|
||
// 爬虫任务分页状态
|
||
let spiderPagination = {
|
||
currentPage: 1,
|
||
pageSize: 10,
|
||
};
|
||
|
||
// 加载爬虫任务列表(带分页)
|
||
async function loadSpiders(page = 1, pageSize = 10) {
|
||
try {
|
||
const response = await fetch(
|
||
`/api/spiders/list?page=${page}&page_size=${pageSize}`
|
||
);
|
||
const result = await response.json();
|
||
|
||
// 更新分页状态
|
||
spiderPagination = {
|
||
currentPage: result.page,
|
||
pageSize: result.page_size,
|
||
total: result.total,
|
||
totalPages: result.total_pages,
|
||
};
|
||
|
||
// 更新表格内容
|
||
const tbody = document.querySelector("#spiderList tbody");
|
||
tbody.innerHTML = "";
|
||
result.items.forEach((spider) => {
|
||
const row = tbody.insertRow();
|
||
row.innerHTML = `
|
||
<td>${spider.task_id}</td>
|
||
<td>${spider.task_name}</td>
|
||
<td>${spider.spider_name}</td>
|
||
<td><span class="status-badge ${getStatusBadgeClass(
|
||
spider.status
|
||
)}">${getStatusText(spider.status)}</span></td>
|
||
<td>${spider.message}</td>
|
||
<td>${spider.start_time || "-"}</td>
|
||
<td>${spider.end_time || "-"}</td>
|
||
`;
|
||
});
|
||
|
||
// 更新分页信息
|
||
updateSpiderPaginationUI();
|
||
} catch (error) {
|
||
showToast(`加载爬虫任务列表失败: ${error.message}`, "danger");
|
||
}
|
||
}
|
||
|
||
// 更新分页UI
|
||
function updateSpiderPaginationUI() {
|
||
const pageInfo = document.getElementById("pageInfo");
|
||
const prevBtn = document.getElementById("prevPage");
|
||
const nextBtn = document.getElementById("nextPage");
|
||
|
||
pageInfo.textContent = `${spiderPagination.currentPage}/${spiderPagination.totalPages}`;
|
||
|
||
// 更新按钮状态
|
||
prevBtn.disabled = spiderPagination.currentPage <= 1;
|
||
nextBtn.disabled =
|
||
spiderPagination.currentPage >= spiderPagination.totalPages;
|
||
}
|
||
|
||
// 分页按钮事件
|
||
document.getElementById("prevPage").addEventListener("click", () => {
|
||
if (spiderPagination.currentPage > 1) {
|
||
loadSpiders(
|
||
spiderPagination.currentPage - 1,
|
||
spiderPagination.pageSize
|
||
);
|
||
}
|
||
});
|
||
|
||
document.getElementById("nextPage").addEventListener("click", () => {
|
||
if (spiderPagination.currentPage < spiderPagination.totalPages) {
|
||
loadSpiders(
|
||
spiderPagination.currentPage + 1,
|
||
spiderPagination.pageSize
|
||
);
|
||
}
|
||
});
|
||
|
||
function getStatusBadgeClass(status) {
|
||
switch (status) {
|
||
case "running":
|
||
return "status-running";
|
||
case "completed":
|
||
return "status-completed";
|
||
case "failed":
|
||
return "status-failed";
|
||
case "pending":
|
||
return "status-pending";
|
||
default:
|
||
return "";
|
||
}
|
||
}
|
||
|
||
function getStatusText(status) {
|
||
switch (status) {
|
||
case "running":
|
||
return "运行中";
|
||
case "completed":
|
||
return "已完成";
|
||
case "failed":
|
||
return "失败";
|
||
case "pending":
|
||
return "等待中";
|
||
default:
|
||
return "未知";
|
||
}
|
||
}
|
||
|
||
// 保存编辑的任务
|
||
document
|
||
.getElementById("editTaskForm")
|
||
.addEventListener("submit", async (e) => {
|
||
e.preventDefault();
|
||
|
||
const taskId = document.getElementById("edit_task_id").value;
|
||
const formData = {
|
||
name: document.getElementById("edit_name").value.trim(),
|
||
cron_expression: document
|
||
.getElementById("edit_cron_expression")
|
||
.value.trim(),
|
||
spider_name: document
|
||
.getElementById("edit_spider_name")
|
||
.value.trim(),
|
||
url: document.getElementById("edit_url").value.trim(),
|
||
video_list: parseInt(
|
||
document.getElementById("edit_video_list").value
|
||
),
|
||
enabled: true, // 保持任务启用状态
|
||
};
|
||
|
||
// 验证必填字段
|
||
const validationErrors = [];
|
||
if (!formData.name) validationErrors.push("任务名称不能为空");
|
||
if (!formData.cron_expression)
|
||
validationErrors.push("Cron表达式不能为空");
|
||
if (!formData.spider_name) validationErrors.push("爬虫名称不能为空");
|
||
if (!formData.url) validationErrors.push("URL不能为空");
|
||
if (isNaN(formData.video_list) || formData.video_list < 0)
|
||
validationErrors.push("剧集ID必须是非负数字");
|
||
|
||
if (validationErrors.length > 0) {
|
||
showToast(validationErrors.join("<br>"), "danger");
|
||
return;
|
||
}
|
||
|
||
// 显示加载状态
|
||
const submitButton = e.target.querySelector('button[type="submit"]');
|
||
const originalText = submitButton.innerHTML;
|
||
submitButton.disabled = true;
|
||
submitButton.innerHTML =
|
||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 保存中...';
|
||
|
||
try {
|
||
const response = await fetch(`/api/scheduled-tasks/${taskId}`, {
|
||
method: "PUT",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify(formData),
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok) {
|
||
showToast("任务更新成功", "success");
|
||
closeEditModal();
|
||
await loadTasks(); // 等待任务列表刷新完成
|
||
} else {
|
||
showToast(`更新失败: ${data.detail || "未知错误"}`, "danger");
|
||
}
|
||
} catch (error) {
|
||
console.error("Error:", error);
|
||
showToast(`更新失败: ${error.message || "网络错误"}`, "danger");
|
||
} finally {
|
||
// 恢复按钮状态
|
||
submitButton.disabled = false;
|
||
submitButton.innerHTML = originalText;
|
||
}
|
||
});
|
||
|
||
// 定期检查任务状态
|
||
async function checkTaskStatus(taskId) {
|
||
try {
|
||
const response = await fetch(`/api/task-status/${taskId}`);
|
||
if (response.ok) {
|
||
const status = await response.json();
|
||
updateTaskStatus(taskId, status.status);
|
||
|
||
// 如果任务仍在运行,继续检查
|
||
if (status.status === "running" || status.status === "pending") {
|
||
setTimeout(() => checkTaskStatus(taskId), 5000); // 每5秒检查一次
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("检查任务状态失败:", error);
|
||
}
|
||
}
|
||
|
||
// 修改runTask函数以支持状态更新
|
||
async function runTask(taskId) {
|
||
try {
|
||
const response = await fetch(`/api/scheduled-tasks/${taskId}/run`, {
|
||
method: "POST",
|
||
});
|
||
|
||
if (response.status === 200) {
|
||
// const result = await response.json();
|
||
showToast("任务已开始运行");
|
||
// 开始检查任务状态
|
||
checkTaskStatus(taskId);
|
||
// 刷新爬虫任务列表
|
||
loadSpiders();
|
||
} else {
|
||
const error = await response.json();
|
||
showToast(`运行失败: ${error.detail}`, "danger");
|
||
}
|
||
} catch (error) {
|
||
showToast(`运行失败: ${error.message}`, "danger");
|
||
}
|
||
}
|
||
|
||
async function toggleTask(taskId, enabled) {
|
||
try {
|
||
const response = await fetch(
|
||
`/api/scheduled-tasks/${taskId}/toggle`,
|
||
{
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({ enabled }),
|
||
}
|
||
);
|
||
|
||
if (response.ok) {
|
||
showToast(`任务已${enabled ? "启用" : "禁用"}`);
|
||
} else {
|
||
const error = await response.json();
|
||
showToast(`切换状态失败: ${error.detail}`, "danger");
|
||
// 恢复复选框状态
|
||
loadTasks();
|
||
}
|
||
} catch (error) {
|
||
showToast(`切换状态失败: ${error.message}`, "danger");
|
||
// 恢复复选框状态
|
||
loadTasks();
|
||
}
|
||
}
|
||
|
||
document
|
||
.getElementById("taskForm")
|
||
.addEventListener("submit", async (e) => {
|
||
e.preventDefault();
|
||
|
||
const formData = {
|
||
name: document.getElementById("name").value,
|
||
cron_expression: document.getElementById("cron_expression").value,
|
||
spider_name: document.getElementById("spider_name").value,
|
||
url: document.getElementById("url").value,
|
||
video_list: parseInt(document.getElementById("video_list").value),
|
||
};
|
||
|
||
try {
|
||
const response = await fetch("/api/scheduled-tasks", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify(formData),
|
||
});
|
||
|
||
if (response.ok) {
|
||
showToast("任务已创建");
|
||
e.target.reset();
|
||
loadTasks();
|
||
} else {
|
||
const error = await response.json();
|
||
showToast(`创建失败: ${error.detail}`, "danger");
|
||
}
|
||
} catch (error) {
|
||
showToast(`创建失败: ${error.message}`, "danger");
|
||
}
|
||
});
|
||
|
||
// 添加表单验证函数
|
||
function validateFormField(input) {
|
||
const field = input.id.replace("edit_", "");
|
||
const value = input.value.trim();
|
||
let isValid = true;
|
||
let errorMessage = "";
|
||
|
||
switch (field) {
|
||
case "name":
|
||
if (!value) {
|
||
isValid = false;
|
||
errorMessage = "任务名称不能为空";
|
||
}
|
||
break;
|
||
case "cron_expression":
|
||
if (!value) {
|
||
isValid = false;
|
||
errorMessage = "Cron表达式不能为空";
|
||
} else if (!/^(\S+\s+){4}\S+$/.test(value)) {
|
||
isValid = false;
|
||
errorMessage = "Cron表达式格式无效";
|
||
}
|
||
break;
|
||
case "spider_name":
|
||
if (!value) {
|
||
isValid = false;
|
||
errorMessage = "爬虫名称不能为空";
|
||
}
|
||
break;
|
||
case "url":
|
||
if (!value) {
|
||
isValid = false;
|
||
errorMessage = "URL不能为空";
|
||
} else {
|
||
try {
|
||
new URL(value);
|
||
} catch {
|
||
isValid = false;
|
||
errorMessage = "URL格式无效";
|
||
}
|
||
}
|
||
break;
|
||
case "video_list":
|
||
if (!value || isNaN(value) || parseInt(value) < 0) {
|
||
isValid = false;
|
||
errorMessage = "剧集ID必须是非负数字";
|
||
}
|
||
break;
|
||
}
|
||
|
||
// 更新输入框状态
|
||
input.classList.remove("is-valid", "is-invalid");
|
||
input.classList.add(isValid ? "is-valid" : "is-invalid");
|
||
|
||
// 更新或创建反馈提示
|
||
let feedback = input.nextElementSibling;
|
||
if (!feedback || !feedback.classList.contains("invalid-feedback")) {
|
||
feedback = document.createElement("div");
|
||
feedback.className = "invalid-feedback";
|
||
input.parentNode.insertBefore(feedback, input.nextSibling);
|
||
}
|
||
feedback.textContent = errorMessage;
|
||
|
||
return isValid;
|
||
}
|
||
|
||
// 为编辑表单的所有输入字段添加验证
|
||
document.querySelectorAll("#editTaskForm input").forEach((input) => {
|
||
input.addEventListener("input", () => validateFormField(input));
|
||
input.addEventListener("blur", () => validateFormField(input));
|
||
});
|
||
|
||
// 修改编辑表单提交验证
|
||
document
|
||
.getElementById("editTaskForm")
|
||
.addEventListener("submit", async (e) => {
|
||
e.preventDefault();
|
||
|
||
// 验证所有字段
|
||
const inputs = e.target.querySelectorAll(
|
||
'input:not([type="hidden"])'
|
||
);
|
||
let isValid = true;
|
||
inputs.forEach((input) => {
|
||
if (!validateFormField(input)) {
|
||
isValid = false;
|
||
}
|
||
});
|
||
|
||
if (!isValid) {
|
||
showToast("请修正表单中的错误", "danger");
|
||
return;
|
||
}
|
||
});
|
||
|
||
// 初始化所有工具提示
|
||
function initTooltips() {
|
||
const tooltipTriggerList = [].slice.call(
|
||
document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||
);
|
||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||
});
|
||
}
|
||
|
||
// 在模态框显示时初始化工具提示
|
||
document
|
||
.getElementById("editModal")
|
||
.addEventListener("shown.bs.modal", function () {
|
||
initTooltips();
|
||
});
|
||
|
||
// 页面加载时获取任务列表
|
||
loadTasks();
|
||
loadSpiders();
|
||
|
||
// 初始化页面上的工具提示
|
||
initTooltips();
|
||
|
||
// 定期刷新爬虫任务列表
|
||
setInterval(loadSpiders, 5000); // 每5秒刷新一次
|
||
</script>
|
||
</body>
|
||
</html>
|