feat: 批量处理页面与导航
- 新增 web/batch.html + batch.js:目录树选择器(懒加载,本地后端 提供 roots/dirs 接口)、工作流下拉、进度与产物下载 - 四个页面导航栏增加"批量处理"入口;styles.css 补齐批量页样式
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
<nav>
|
||||
<a href="/">应用中心</a>
|
||||
<a href="/tasks.html">任务管理</a>
|
||||
<a href="/batch.html">批量处理</a>
|
||||
<a href="/admin.html">管理后台</a>
|
||||
<a href="/workflow.html">工作流</a>
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
// VRSub 批量处理页脚本:创建/列表/暂停/继续/删除批量任务,展开查看每视频明细。
|
||||
// 复用 app.js 的 api/escapeHtml/badge/formatTime/formatElapsed/progressBar 辅助函数。
|
||||
|
||||
// 已发布工作流缓存(id → 名称),用于任务列表展示工作流名。
|
||||
const WORKFLOW_NAMES = new Map();
|
||||
|
||||
// 已展开明细的任务 ID 集合(防止轮询刷新时折叠用户展开的行)。
|
||||
const expandedJobs = new Set();
|
||||
|
||||
// 加载已发布工作流填充下拉框;同时缓存 id → 名称供列表展示。
|
||||
async function loadBatchWorkflowOptions() {
|
||||
const select = document.getElementById("batchWorkflow");
|
||||
if (!select) return;
|
||||
const apps = await api("/api/apps");
|
||||
select.innerHTML = apps.length
|
||||
? apps
|
||||
.map((app) => `<option value="${escapeHtml(app.id)}">${escapeHtml(app.name)}</option>`)
|
||||
.join("")
|
||||
: '<option value="">暂无已发布工作流</option>';
|
||||
apps.forEach((app) => WORKFLOW_NAMES.set(app.id, app.name));
|
||||
}
|
||||
|
||||
// 创建批量任务:POST 文件夹路径与所选工作流,成功后清空输入并刷新列表。
|
||||
async function createBatchJob() {
|
||||
const button = document.getElementById("batchStart");
|
||||
const hint = document.getElementById("batchCreateHint");
|
||||
const folder = document.getElementById("batchFolder").value.trim();
|
||||
const workflowId = document.getElementById("batchWorkflow").value;
|
||||
const recursive = document.getElementById("batchRecursive").checked;
|
||||
if (!folder) {
|
||||
hint.textContent = "请填写视频文件夹路径";
|
||||
return;
|
||||
}
|
||||
if (!workflowId) {
|
||||
hint.textContent = "请先在工作流页发布一个工作流";
|
||||
return;
|
||||
}
|
||||
button.disabled = true;
|
||||
hint.textContent = "正在扫描视频并创建任务…";
|
||||
try {
|
||||
const job = await api("/api/batch/jobs", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ folder, workflow_id: workflowId, recursive }),
|
||||
});
|
||||
hint.textContent = `已创建任务 ${job.id},共 ${job.videos.length} 个视频`;
|
||||
document.getElementById("batchFolder").value = "";
|
||||
} catch (error) {
|
||||
hint.textContent = `创建失败:${error.message}`;
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
await loadBatchJobs();
|
||||
}
|
||||
|
||||
// 渲染单个任务的进度:已完成/跳过与失败计入不同颜色,总数为任务视频数。
|
||||
function batchProgress(job) {
|
||||
const total = job.total || 0;
|
||||
const done = job.done || 0;
|
||||
const failed = job.failed || 0;
|
||||
if (!total) return "待扫描";
|
||||
const percent = Math.round(((done + failed) / total) * 100);
|
||||
// 有失败视频时追加红色失败计数,避免把"部分失败"误看成全部完成。
|
||||
const failedText = failed > 0 ? ` <span class="danger">,失败 ${failed}</span>` : "";
|
||||
return `${progressBar(percent, job.status === "FAILED")} ${done}/${total} 完成 ${percent}%${failedText}`;
|
||||
}
|
||||
|
||||
// 渲染任务的暂停/继续/删除/详情按钮。
|
||||
function batchActions(job) {
|
||||
const id = encodeURIComponent(job.id);
|
||||
const canPause = job.status === "RUNNING" || job.status === "QUEUED";
|
||||
const canResume = job.status === "PAUSED";
|
||||
return `
|
||||
${canPause ? `<button class="warn" data-batch-pause="${id}">暂停</button>` : ""}
|
||||
${canResume ? `<button class="warn" data-batch-resume="${id}">继续</button>` : ""}
|
||||
<button class="danger" data-batch-delete="${id}">删除</button>
|
||||
<button data-batch-detail="${id}">详情</button>
|
||||
`;
|
||||
}
|
||||
|
||||
// 加载批量任务列表并渲染;已展开的任务自动重新拉取明细(展开行内嵌)。
|
||||
async function loadBatchJobs() {
|
||||
const tbody = document.getElementById("batchJobList");
|
||||
if (!tbody) return;
|
||||
const jobs = await api("/api/batch/jobs");
|
||||
tbody.innerHTML = jobs.length
|
||||
? (
|
||||
await Promise.all(
|
||||
jobs.map(async (job) => {
|
||||
const statusHtml =
|
||||
job.status === "COMPLETED" && (job.failed || 0) > 0
|
||||
? `<span class="badge error">部分失败</span> <span class="muted">${job.failed} 个视频失败,展开详情查看</span>`
|
||||
: job.status === "RUNNING"
|
||||
? `${badge(job.status)} <span class="muted">处理中 ${formatElapsed(job.updated_at)}</span>`
|
||||
: job.status === "QUEUED"
|
||||
? `${badge(job.status)} <span class="muted">排队中</span>`
|
||||
: job.status === "PAUSED"
|
||||
? `${badge(job.status)} <span class="muted">已暂停 ${formatElapsed(job.updated_at)}</span>`
|
||||
: badge(job.status);
|
||||
const current = job.current_video ? PathBase(job.current_video) : "-";
|
||||
const row = `
|
||||
<tr>
|
||||
<td title="${escapeHtml(job.error || "")}">${escapeHtml(job.id)}</td>
|
||||
<td title="${escapeHtml(job.folder_path)}">${escapeHtml(PathBase(job.folder_path))}</td>
|
||||
<td>${escapeHtml(WORKFLOW_NAMES.get(job.workflow_id) || job.workflow_id)}</td>
|
||||
<td>${statusHtml}</td>
|
||||
<td>${batchProgress(job)}</td>
|
||||
<td>${escapeHtml(current)}</td>
|
||||
<td>${escapeHtml(formatTime(job.created_at))}</td>
|
||||
<td>${batchActions(job)}</td>
|
||||
</tr>`;
|
||||
// 展开中的任务在下方追加明细行(每视频状态与产物下载)。
|
||||
const detailRow = expandedJobs.has(job.id)
|
||||
? `<tr class="batch-detail-row"><td colspan="8">${await videoDetailHtml(job.id)}</td></tr>`
|
||||
: "";
|
||||
return row + detailRow;
|
||||
}),
|
||||
)
|
||||
).join("")
|
||||
: '<tr><td colspan="8">暂无批量任务,请在上方创建。</td></tr>';
|
||||
}
|
||||
|
||||
// 取路径的最后一段(兼容 Windows 反斜杠与 Unix 斜杠)。
|
||||
function PathBase(path) {
|
||||
return String(path).split(/[\\/]/).pop() || path;
|
||||
}
|
||||
|
||||
// 拉取任务详情并渲染视频明细表:文件、状态、错误与产物下载链接。
|
||||
async function videoDetailHtml(jobId) {
|
||||
const job = await api(`/api/batch/jobs/${encodeURIComponent(jobId)}`);
|
||||
const videos = job.videos || [];
|
||||
if (!videos.length) return "(暂无视频)";
|
||||
const rows = videos
|
||||
.map((video) => {
|
||||
const finals = video.finals || {};
|
||||
const links = Object.keys(finals)
|
||||
.map(
|
||||
(alias) =>
|
||||
`<a class="download-link" href="/api/batch/jobs/${encodeURIComponent(jobId)}/videos/${encodeURIComponent(video.id)}/download?alias=${encodeURIComponent(alias)}">${escapeHtml(alias)}</a>`,
|
||||
)
|
||||
.join(" ");
|
||||
return `
|
||||
<tr>
|
||||
<td>${escapeHtml(PathBase(video.video_path))}</td>
|
||||
<td>${badge(video.status)}</td>
|
||||
<td title="${escapeHtml(video.error || "")}">${escapeHtml(video.error || "-")}</td>
|
||||
<td>${links || "-"}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
return `<table class="inner-table"><thead><tr><th>视频</th><th>状态</th><th>错误</th><th>产物</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||
}
|
||||
|
||||
// 暂停批量任务:当前 run 在分块/帧边界停下,后续视频不再开始。
|
||||
async function pauseBatchJob(jobId) {
|
||||
try {
|
||||
const result = await api(`/api/batch/jobs/${jobId}/pause`, { method: "POST" });
|
||||
alert(`批量任务 ${result.id} 已暂停,继续后从未完成处接着处理`);
|
||||
} catch (error) {
|
||||
alert(`暂停失败:${error.message}`);
|
||||
}
|
||||
await loadBatchJobs();
|
||||
}
|
||||
|
||||
// 继续批量任务:恢复排队,从上次断点(未完成视频)继续处理。
|
||||
async function resumeBatchJob(jobId) {
|
||||
try {
|
||||
const result = await api(`/api/batch/jobs/${jobId}/resume`, { method: "POST" });
|
||||
alert(`批量任务 ${result.id} 已恢复执行`);
|
||||
} catch (error) {
|
||||
alert(`继续失败:${error.message}`);
|
||||
}
|
||||
await loadBatchJobs();
|
||||
}
|
||||
|
||||
// 删除批量任务:只清理数据库记录,磁盘上的同名文件夹与产物保留。
|
||||
async function deleteBatchJob(jobId) {
|
||||
if (!confirm("删除批量任务?磁盘上的产物文件会保留。")) return;
|
||||
try {
|
||||
await api(`/api/batch/jobs/${jobId}`, { method: "DELETE" });
|
||||
} catch (error) {
|
||||
alert(`删除失败:${error.message}`);
|
||||
}
|
||||
await loadBatchJobs();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 目录树选择器:点击"选择文件夹"按钮弹出,懒加载浏览本地目录后回填路径。
|
||||
// 浏览器拿不到文件夹绝对路径,目录列表由本地后端(/api/batch/roots、dirs)提供。
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// 当前选中的目录绝对路径(未选中时为 null)。
|
||||
let dirPickerSelection = null;
|
||||
|
||||
// 打开目录树选择器:显示模态框并加载可浏览根目录。
|
||||
async function openDirPicker() {
|
||||
document.getElementById("dirPicker").classList.remove("hidden");
|
||||
const tree = document.getElementById("dirTree");
|
||||
tree.innerHTML = '<div class="muted">加载中…</div>';
|
||||
dirPickerSelection = null;
|
||||
document.getElementById("dirPickerUse").disabled = true;
|
||||
document.getElementById("dirPickerSelected").textContent = "";
|
||||
try {
|
||||
const roots = await api("/api/batch/roots");
|
||||
tree.innerHTML = "";
|
||||
roots.forEach((root) => tree.appendChild(dirNode(root, 0)));
|
||||
} catch (error) {
|
||||
tree.innerHTML = `<div class="muted">加载失败:${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭目录树选择器。
|
||||
function closeDirPicker() {
|
||||
document.getElementById("dirPicker").classList.add("hidden");
|
||||
}
|
||||
|
||||
// 渲染一个目录节点行:缩进 + 展开箭头 + 名称;点击行选中并切换展开。
|
||||
function dirNode(dir, depth) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "dir-node";
|
||||
row.dataset.path = dir.path;
|
||||
row.dataset.expanded = "false";
|
||||
|
||||
const indent = document.createElement("span");
|
||||
indent.className = "dir-indent";
|
||||
indent.style.width = `${depth * 18}px`;
|
||||
const arrow = document.createElement("span");
|
||||
arrow.className = "dir-arrow";
|
||||
arrow.textContent = "▸";
|
||||
const name = document.createElement("span");
|
||||
name.className = "dir-name";
|
||||
name.textContent = dir.name;
|
||||
name.title = dir.path;
|
||||
|
||||
row.append(indent, arrow, name);
|
||||
row.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
selectDir(row);
|
||||
// 首次点击展开子目录(懒加载),再次点击折叠。
|
||||
if (row.dataset.expanded === "false") {
|
||||
expandDir(row, depth);
|
||||
} else {
|
||||
collapseDir(row);
|
||||
}
|
||||
});
|
||||
return row;
|
||||
}
|
||||
|
||||
// 选中目录行:取消其它行高亮,更新底部展示与"使用此文件夹"按钮。
|
||||
function selectDir(row) {
|
||||
document.querySelectorAll("#dirTree .dir-node.selected").forEach((item) => item.classList.remove("selected"));
|
||||
row.classList.add("selected");
|
||||
dirPickerSelection = row.dataset.path;
|
||||
document.getElementById("dirPickerSelected").textContent = row.dataset.path;
|
||||
document.getElementById("dirPickerUse").disabled = false;
|
||||
}
|
||||
|
||||
// 展开目录:请求后端列出子目录,把子节点行插到当前行之后。
|
||||
async function expandDir(row, depth) {
|
||||
const arrow = row.querySelector(".dir-arrow");
|
||||
arrow.textContent = "…";
|
||||
let children;
|
||||
try {
|
||||
const data = await api(`/api/batch/dirs?path=${encodeURIComponent(row.dataset.path)}`);
|
||||
children = document.createElement("div");
|
||||
children.className = "dir-children";
|
||||
data.dirs.forEach((child) => children.appendChild(dirNode(child, depth + 1)));
|
||||
if (!data.dirs.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "dir-empty muted";
|
||||
empty.textContent = "(无子目录)";
|
||||
children.appendChild(empty);
|
||||
}
|
||||
} catch (error) {
|
||||
children = document.createElement("div");
|
||||
children.className = "dir-children";
|
||||
const err = document.createElement("div");
|
||||
err.className = "dir-empty muted";
|
||||
err.textContent = `加载失败:${error.message}`;
|
||||
children.appendChild(err);
|
||||
}
|
||||
row.dataset.expanded = "true";
|
||||
arrow.textContent = "▾";
|
||||
row.insertAdjacentElement("afterend", children);
|
||||
}
|
||||
|
||||
// 折叠目录:移除已展开的子节点行。
|
||||
function collapseDir(row) {
|
||||
row.dataset.expanded = "false";
|
||||
row.querySelector(".dir-arrow").textContent = "▸";
|
||||
const children = row.nextElementSibling;
|
||||
if (children && children.classList.contains("dir-children")) {
|
||||
children.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用当前选中目录:回填路径输入框并关闭选择器。
|
||||
function useDirPicker() {
|
||||
if (!dirPickerSelection) return;
|
||||
document.getElementById("batchFolder").value = dirPickerSelection;
|
||||
closeDirPicker();
|
||||
}
|
||||
|
||||
// 展开/折叠任务明细(事件委托处理列表内的按钮)。
|
||||
async function onBatchJobClick(event) {
|
||||
const target = event.target;
|
||||
const pause = target.dataset.batchPause;
|
||||
const resume = target.dataset.batchResume;
|
||||
const del = target.dataset.batchDelete;
|
||||
const detail = target.dataset.batchDetail;
|
||||
if (pause) return pauseBatchJob(pause);
|
||||
if (resume) return resumeBatchJob(resume);
|
||||
if (del) return deleteBatchJob(del);
|
||||
if (detail) {
|
||||
// 切换展开状态后刷新列表(展开行会拉取最新明细)。
|
||||
if (expandedJobs.has(detail)) {
|
||||
expandedJobs.delete(detail);
|
||||
} else {
|
||||
expandedJobs.add(detail);
|
||||
}
|
||||
await loadBatchJobs();
|
||||
}
|
||||
}
|
||||
|
||||
// 页面初始化:绑定事件并启动列表轮询(每 2 秒刷新一次进度)。
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
const startButton = document.getElementById("batchStart");
|
||||
if (!startButton) return;
|
||||
startButton.addEventListener("click", createBatchJob);
|
||||
// 目录树选择器:打开/关闭/使用/点击遮罩关闭。
|
||||
document.getElementById("batchBrowse").addEventListener("click", openDirPicker);
|
||||
document.getElementById("dirPickerClose").addEventListener("click", closeDirPicker);
|
||||
document.getElementById("dirPickerUse").addEventListener("click", useDirPicker);
|
||||
document.getElementById("dirPicker").addEventListener("click", (event) => {
|
||||
// 点击遮罩(面板外部)关闭选择器。
|
||||
if (event.target.id === "dirPicker") closeDirPicker();
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
// Esc 关闭选择器。
|
||||
if (event.key === "Escape") closeDirPicker();
|
||||
});
|
||||
document.getElementById("batchJobList").addEventListener("click", onBatchJobClick);
|
||||
await loadBatchWorkflowOptions();
|
||||
await loadBatchJobs();
|
||||
setInterval(loadBatchJobs, 2000);
|
||||
});
|
||||
@@ -346,3 +346,150 @@ th {
|
||||
pointer-events: auto;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
/* 批量处理页:明细行的内嵌表格样式。 */
|
||||
.batch-detail-row td {
|
||||
background: #fafbfc;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
/* 内嵌表格:无边框、行距紧凑,嵌入批量任务明细行中。 */
|
||||
.inner-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.inner-table th {
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
padding: 4px 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.inner-table td {
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
border-bottom: 1px solid #f0f1f3;
|
||||
}
|
||||
|
||||
/* 批量页创建表单的复选框与标签同行展示。 */
|
||||
.form-row .inline {
|
||||
margin-left: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 批量页:路径输入框 + "选择文件夹"按钮同行。 */
|
||||
.path-picker {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.path-picker input {
|
||||
flex: 1;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
/* 目录树选择器:全屏遮罩 + 居中面板。 */
|
||||
.dir-picker {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dir-picker.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dir-picker-panel {
|
||||
width: min(560px, 92vw);
|
||||
max-height: 70vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dir-picker-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* 关闭按钮(✕):无边框文本按钮。 */
|
||||
.dir-picker-head .plain {
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.dir-tree {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 8px 0;
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
/* 目录节点行:悬停高亮,选中项加深。 */
|
||||
.dir-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dir-node:hover {
|
||||
background: #f3f6fa;
|
||||
}
|
||||
|
||||
.dir-node.selected {
|
||||
background: #e3edf7;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dir-arrow {
|
||||
width: 20px;
|
||||
color: #888;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.dir-name {
|
||||
margin-left: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.dir-empty {
|
||||
padding: 4px 12px 4px 56px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dir-picker-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.dir-picker-foot .muted {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<!doctype html>
|
||||
<!-- VRSub 批量处理页:输入文件夹路径批量处理全部视频,不上传副本。 -->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>VRSub - 批量处理</title>
|
||||
<link rel="stylesheet" href="/assets/styles.css?v=3" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/">VRSub</a>
|
||||
<nav>
|
||||
<a href="/">发起任务</a>
|
||||
<a href="/tasks.html">任务管理</a>
|
||||
<a href="/batch.html">批量处理</a>
|
||||
<a href="/admin.html">管理后台</a>
|
||||
<a href="/workflow.html">工作流</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<h1>批量处理</h1>
|
||||
<p class="muted">
|
||||
直接读取所选文件夹下的全部视频(<b>不上传副本</b>),逐个执行所选流水线;
|
||||
每个视频的中间态数据与最终产物存放在视频旁边的同名文件夹(如 movie.mp4 →
|
||||
movie/)。已处理过的视频自动跳过;暂停后重新开始时,从未完成的视频继续。
|
||||
</p>
|
||||
|
||||
<!-- 创建批量任务:文件夹路径 + 工作流选择 + 是否递归。 -->
|
||||
<section class="panel">
|
||||
<h2>创建批量任务</h2>
|
||||
<div class="form-row">
|
||||
<label for="batchFolder">视频文件夹路径</label>
|
||||
<div class="path-picker">
|
||||
<input id="batchFolder" type="text" readonly placeholder="点击右侧按钮选择文件夹" />
|
||||
<button id="batchBrowse" type="button">选择文件夹…</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="batchWorkflow">处理流水线</label>
|
||||
<select id="batchWorkflow"></select>
|
||||
<label class="inline"><input id="batchRecursive" type="checkbox" checked /> 包含子文件夹</label>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<button id="batchStart" class="primary">开始批量处理</button>
|
||||
<span id="batchCreateHint" class="muted"></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 目录树选择器:点击"选择文件夹"按钮弹出,懒加载浏览本地目录。 -->
|
||||
<div id="dirPicker" class="dir-picker hidden">
|
||||
<div class="dir-picker-panel">
|
||||
<div class="dir-picker-head">
|
||||
<b>选择视频文件夹</b>
|
||||
<button id="dirPickerClose" type="button" class="plain">✕</button>
|
||||
</div>
|
||||
<div id="dirTree" class="dir-tree"></div>
|
||||
<div class="dir-picker-foot">
|
||||
<span id="dirPickerSelected" class="muted"></span>
|
||||
<button id="dirPickerUse" type="button" disabled>使用此文件夹</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量任务列表:状态、进度与暂停/继续操作,可展开查看每视频明细。 -->
|
||||
<section class="panel">
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>任务 ID</th>
|
||||
<th>文件夹</th>
|
||||
<th>工作流</th>
|
||||
<th>状态</th>
|
||||
<th>进度</th>
|
||||
<th>当前视频</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="batchJobList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/assets/app.js?v=3"></script>
|
||||
<script src="/assets/batch.js?v=1"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -15,6 +15,7 @@
|
||||
<nav>
|
||||
<a href="/">发起任务</a>
|
||||
<a href="/tasks.html">任务管理</a>
|
||||
<a href="/batch.html">批量处理</a>
|
||||
<a href="/admin.html">管理后台</a>
|
||||
<a href="/workflow.html">工作流</a>
|
||||
</nav>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<nav>
|
||||
<a href="/">发起任务</a>
|
||||
<a href="/tasks.html">任务管理</a>
|
||||
<a href="/batch.html">批量处理</a>
|
||||
<a href="/admin.html">管理后台</a>
|
||||
<a href="/workflow.html">工作流</a>
|
||||
</nav>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<nav>
|
||||
<a href="/">应用中心</a>
|
||||
<a href="/tasks.html">任务管理</a>
|
||||
<a href="/batch.html">批量处理</a>
|
||||
<a href="/admin.html">管理后台</a>
|
||||
<a href="/workflow.html">工作流</a>
|
||||
</nav>
|
||||
|
||||
Reference in New Issue
Block a user