- download_stat 新增 _task_queue / _completed_chats 与三个辅助函数 - 切频道前自动把上一个频道的最终进度快照进 completed_chats - save_and_restart(_multi) 启动任务时设置队列,供前端渲染总览 - 前端 banner 改为"任务队列"多卡片列表(✅已完成 / 🚀当前 / ⏳排队中), 含进度条、跳过明细;兼容单频道场景 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+152
-72
@@ -195,17 +195,42 @@
|
||||
.card-hd-badge { font-size: 11px; font-weight: 400; color: var(--muted); }
|
||||
.card-bd { padding: 12px 14px; }
|
||||
|
||||
/* 任务横幅 */
|
||||
/* 任务队列总览 */
|
||||
.task-banner {
|
||||
display: none; padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
}
|
||||
.task-banner.show { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.task-banner-name { font-weight: 600; color: var(--accent); font-size: 13px; }
|
||||
.task-banner-filter { font-size: 11px; color: var(--muted); margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 480px; }
|
||||
.task-banner-state { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
||||
.task-banner-left { min-width: 0; flex: 1; }
|
||||
.task-banner.show { display: block; }
|
||||
.qbanner-head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
font-size: 12px; color: var(--muted); margin-bottom: 8px;
|
||||
}
|
||||
.qbanner-title { font-weight: 600; color: var(--text); font-size: 13px; }
|
||||
.qbanner-summary { font-size: 11px; }
|
||||
|
||||
/* 队列卡片(已完成 / 当前 / 排队中) */
|
||||
.qcard {
|
||||
display: flex; align-items: flex-start; gap: 10px;
|
||||
padding: 8px 10px; border-radius: 6px;
|
||||
margin-bottom: 6px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.qcard:last-child { margin-bottom: 0; }
|
||||
.qcard-icon { font-size: 14px; line-height: 1.5; flex-shrink: 0; }
|
||||
.qcard-main { flex: 1; min-width: 0; }
|
||||
.qcard-name { font-weight: 600; font-size: 13px; color: var(--text);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.qcard-state { font-size: 11px; color: var(--muted); margin-top: 2px; }
|
||||
.qcard-prog {
|
||||
height: 4px; background: rgba(255,255,255,.12);
|
||||
border-radius: 2px; overflow: hidden; margin-top: 6px;
|
||||
}
|
||||
.qcard-prog-fill { height: 100%; width: 0%; background: #4caf50; transition: width .3s; }
|
||||
.qcard-active { background: rgba(74,144,226,.08); border-color: rgba(74,144,226,.25); }
|
||||
.qcard-active .qcard-name { color: var(--accent); }
|
||||
.qcard-done { background: rgba(76,175,80,.07); opacity: .85; }
|
||||
.qcard-pending { background: rgba(128,128,128,.06); opacity: .6; }
|
||||
|
||||
/* 历史频道卡片 */
|
||||
.hist-head {
|
||||
@@ -687,9 +712,6 @@
|
||||
/* 下载项操作按钮加大便于点按 */
|
||||
.dl-btn { width: 28px; height: 28px; font-size: 13px; }
|
||||
|
||||
/* 任务横幅过滤表达式不限宽 */
|
||||
.task-banner-filter { max-width: none; }
|
||||
|
||||
/* 列表高度减小 */
|
||||
#dl-list, #done-list { height: 220px; }
|
||||
|
||||
@@ -980,23 +1002,13 @@
|
||||
<!-- ════ MAIN CONTENT ════ -->
|
||||
<div class="main-scroll">
|
||||
|
||||
<!-- 当前任务横幅 -->
|
||||
<!-- 任务队列总览 -->
|
||||
<div class="task-banner" id="task-banner">
|
||||
<div class="task-banner-left">
|
||||
<div class="task-banner-name" id="banner-chat">—</div>
|
||||
<div class="task-banner-filter" id="banner-filter" style="display:none"></div>
|
||||
<div class="task-banner-state" id="banner-state"></div>
|
||||
<!-- 进度条:estimated_total > 0 才展示,展示 已完成 X / N -->
|
||||
<div id="banner-progress"
|
||||
style="display:none;margin-top:6px;display:flex;align-items:center;gap:8px;">
|
||||
<div style="flex:1;height:6px;background:rgba(255,255,255,.12);border-radius:3px;overflow:hidden;">
|
||||
<div id="tbp-fill"
|
||||
style="height:100%;width:0%;background:#4caf50;transition:width .3s;"></div>
|
||||
</div>
|
||||
<div id="tbp-text" style="font-size:11px;color:var(--muted);white-space:nowrap;">0 / 0</div>
|
||||
</div>
|
||||
<div class="qbanner-head">
|
||||
<span class="qbanner-title">任务队列</span>
|
||||
<span class="qbanner-summary" id="qbanner-summary"></span>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--muted);flex-shrink:0;">当前任务</div>
|
||||
<div id="qbanner-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- 下载队列(待处理) -->
|
||||
@@ -1126,13 +1138,6 @@
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
function updateBannerFilter() {
|
||||
const el = document.getElementById('banner-filter');
|
||||
const txt = parseFilterDisplay(currentFilter);
|
||||
if (txt) { el.textContent = txt; el.style.display = ''; }
|
||||
else { el.textContent = ''; el.style.display = 'none'; }
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
fetch('/api/get_config').then(r => r.json()).then(d => {
|
||||
const p = d.save_path || '默认路径';
|
||||
@@ -1140,7 +1145,6 @@
|
||||
el.textContent = p; el.title = p;
|
||||
document.getElementById('path-input').value = d.save_path || '';
|
||||
currentFilter = d.download_filter || '';
|
||||
updateBannerFilter();
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
@@ -1448,6 +1452,121 @@
|
||||
}).catch(() => {}).finally(() => { btn.disabled = false; });
|
||||
}
|
||||
|
||||
// ── 任务队列总览渲染 ──
|
||||
function renderTaskQueue(p) {
|
||||
const banner = document.getElementById('task-banner');
|
||||
const list = document.getElementById('qbanner-list');
|
||||
const summary = document.getElementById('qbanner-summary');
|
||||
|
||||
const queue = Array.isArray(p.task_queue) ? p.task_queue : [];
|
||||
const completed = Array.isArray(p.completed_chats) ? p.completed_chats : [];
|
||||
const currentChat = p.current_chat || '';
|
||||
|
||||
// 若没有队列也没有当前任务,隐藏整个 banner
|
||||
if (!queue.length && !currentChat) {
|
||||
banner.classList.remove('show');
|
||||
list.innerHTML = '';
|
||||
summary.textContent = '';
|
||||
return;
|
||||
}
|
||||
banner.classList.add('show');
|
||||
|
||||
// completed 按 chat_id 建索引
|
||||
const completedMap = {};
|
||||
completed.forEach(c => { completedMap[c.chat_id] = c; });
|
||||
|
||||
// 兜底:队列为空但有 current(多频道队列元信息还没写入时),用 current 顶一下
|
||||
const effectiveQueue = queue.length
|
||||
? queue
|
||||
: [{ chat_id: currentChat, chat_title: p.current_chat_title || currentChat }];
|
||||
|
||||
const paused = dlState === 'continue';
|
||||
let doneCount = 0, runningCount = 0;
|
||||
|
||||
list.innerHTML = effectiveQueue.map(q => {
|
||||
if (completedMap[q.chat_id]) {
|
||||
doneCount++;
|
||||
return renderDoneCard(q, completedMap[q.chat_id]);
|
||||
} else if (q.chat_id === currentChat) {
|
||||
runningCount++;
|
||||
return renderActiveCard(q, p, paused);
|
||||
} else {
|
||||
return renderPendingCard(q);
|
||||
}
|
||||
}).join('');
|
||||
|
||||
summary.textContent = `${doneCount}/${effectiveQueue.length} 完成`;
|
||||
}
|
||||
|
||||
function renderDoneCard(q, c) {
|
||||
const title = escapeHtml(q.chat_title || q.chat_id);
|
||||
const skipStr = (c.existing_skip && c.existing_skip > 0)
|
||||
? `跳过${c.skip||0},其中本次跳过${c.existing_skip}`
|
||||
: `跳过${c.skip||0}`;
|
||||
const totalStr = (c.total && c.total > 0)
|
||||
? `${c.done||0} / ${c.total} 完成`
|
||||
: `${c.done||0} 个完成`;
|
||||
return `
|
||||
<div class="qcard qcard-done">
|
||||
<div class="qcard-icon">✅</div>
|
||||
<div class="qcard-main">
|
||||
<div class="qcard-name">${title}</div>
|
||||
<div class="qcard-state">${totalStr},${skipStr}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderActiveCard(q, p, paused) {
|
||||
const title = escapeHtml(q.chat_title || p.current_chat_title || q.chat_id);
|
||||
const dl = p.downloading_files||0, done = p.completed_files||0, skip = p.skipped_files||0;
|
||||
const qual = p.qualified_files||0, est = p.estimated_total||0;
|
||||
const existingSkip = p.existing_skipped || 0;
|
||||
const rawTotal = est || qual;
|
||||
const realTotal = Math.max(0, rawTotal - existingSkip);
|
||||
const doneStr = rawTotal ? `${done} / ${realTotal}` : `${done}`;
|
||||
const skipStr = existingSkip > 0 ? `跳过${skip},其中本次跳过${existingSkip}` : `跳过${skip}`;
|
||||
|
||||
let st = '';
|
||||
if (p.is_checking && paused) st = `⏸ 已暂停 · 扫描中… (${skipStr})`;
|
||||
else if (p.is_checking && dl>0) st = `🔍 扫描+下载中 (${dl}个,${doneStr},${skipStr})`;
|
||||
else if (p.is_checking) st = `🔍 扫描中… (${doneStr},${skipStr})`;
|
||||
else if (paused) st = `⏸ 已暂停 (${doneStr},${skipStr})`;
|
||||
else if (dl>0) st = `🚀 下载中 (${dl}个,${doneStr},${skipStr})`;
|
||||
else if (done>0||skip>0) st = `✅ 完成 (${doneStr},${skipStr})`;
|
||||
else st = '⏳ 等待开始';
|
||||
|
||||
let progBar = '';
|
||||
if (realTotal > 0) {
|
||||
const pct = Math.min(100, Math.round(done * 100 / realTotal));
|
||||
const suffix = (!est && p.is_checking) ? '(扫描中…)' : '';
|
||||
progBar = `
|
||||
<div class="qcard-prog"><div class="qcard-prog-fill" style="width:${pct}%"></div></div>
|
||||
<div class="qcard-state" style="margin-top:3px;">${done} / ${realTotal}${suffix}</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="qcard qcard-active">
|
||||
<div class="qcard-icon">🚀</div>
|
||||
<div class="qcard-main">
|
||||
<div class="qcard-name">${title}</div>
|
||||
<div class="qcard-state">${st}</div>
|
||||
${progBar}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderPendingCard(q) {
|
||||
const title = escapeHtml(q.chat_title || q.chat_id);
|
||||
return `
|
||||
<div class="qcard qcard-pending">
|
||||
<div class="qcard-icon">⏳</div>
|
||||
<div class="qcard-main">
|
||||
<div class="qcard-name">${title}</div>
|
||||
<div class="qcard-state">排队中…</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── 轮询 ──
|
||||
function poll() {
|
||||
fetch('/get_download_status').then(r => r.json()).then(d => {
|
||||
@@ -1457,46 +1576,7 @@
|
||||
|
||||
fetch('/api/task_progress').then(r => r.json()).then(p => {
|
||||
document.getElementById('h-skip').textContent = p.skipped_files || 0;
|
||||
const banner = document.getElementById('task-banner');
|
||||
const paused = dlState === 'continue';
|
||||
if (p.current_chat) {
|
||||
banner.classList.add('show');
|
||||
const title = p.current_chat_title || p.current_chat;
|
||||
document.getElementById('banner-chat').textContent = title;
|
||||
const dl = p.downloading_files||0, done = p.completed_files||0, skip = p.skipped_files||0;
|
||||
const qual = p.qualified_files||0, est = p.estimated_total||0;
|
||||
const existingSkip = p.existing_skipped || 0;
|
||||
// 分母优先用缓存值;没有就用当次遍历实时累加的 qualified。再扣除"本次任务已跳过",得到真正要下载的数量
|
||||
const rawTotal = est || qual;
|
||||
const realTotal = Math.max(0, rawTotal - existingSkip);
|
||||
const doneStr = rawTotal ? `${done} / ${realTotal}` : `${done}`;
|
||||
// 跳过文案:有本次跳过时追加说明
|
||||
const skipStr = existingSkip > 0 ? `跳过${skip},其中本次跳过${existingSkip}` : `跳过${skip}`;
|
||||
let st = '';
|
||||
if (p.is_checking && paused) st = `⏸ 已暂停 · 扫描中… (${skipStr})`;
|
||||
else if (p.is_checking && dl>0) st = `🔍 扫描+下载中 (${dl}个,${doneStr},${skipStr})`;
|
||||
else if (p.is_checking) st = `🔍 扫描中… (${doneStr},${skipStr})`;
|
||||
else if (paused) st = `⏸ 已暂停 (${doneStr},${skipStr})`;
|
||||
else if (dl>0) st = `🚀 下载中 (${dl}个,${doneStr},${skipStr})`;
|
||||
else if (done>0||skip>0) st = `✅ 完成 (${doneStr},${skipStr})`;
|
||||
else st = '⏳ 等待开始';
|
||||
document.getElementById('banner-state').textContent = st;
|
||||
|
||||
// 进度条:有总数才显示;扫描中且无缓存时给"扫描中…"后缀
|
||||
const prog = document.getElementById('banner-progress');
|
||||
if (realTotal > 0) {
|
||||
prog.style.display = 'flex';
|
||||
const pct = Math.min(100, Math.round(done * 100 / realTotal));
|
||||
document.getElementById('tbp-fill').style.width = pct + '%';
|
||||
const suffix = (!est && p.is_checking) ? '(扫描中…)' : '';
|
||||
document.getElementById('tbp-text').textContent = `${done} / ${realTotal}${suffix}`;
|
||||
} else {
|
||||
prog.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
banner.classList.remove('show');
|
||||
document.getElementById('banner-progress').style.display = 'none';
|
||||
}
|
||||
renderTaskQueue(p);
|
||||
}).catch(() => {});
|
||||
|
||||
fetch('/get_download_list?already_down=false').then(r => r.json()).then(data => {
|
||||
|
||||
Reference in New Issue
Block a user