feat: 支持多频道队列下载
部署到群晖 / deploy (push) Successful in 51s

This commit is contained in:
yuming
2026-04-23 09:56:40 +08:00
parent 2f70a6627e
commit a1499a431f
2 changed files with 294 additions and 27 deletions
+186 -27
View File
@@ -580,6 +580,39 @@
padding: 14px 24px; border-top: 1px solid var(--border); gap: 10px;
}
/* ════ 下载队列 ════ */
.submit-row { display: flex; gap: 6px; align-items: stretch; }
.btn-add-queue {
padding: 9px 14px; border-radius: 7px;
background: transparent; border: 1px solid var(--border);
color: var(--muted); font-size: 13px; font-weight: 600;
cursor: pointer; transition: all .15s; white-space: nowrap;
}
.btn-add-queue:hover { border-color: var(--accent); color: var(--accent); }
.queue-item {
display: flex; align-items: center; gap: 10px;
padding: 9px 2px; border-bottom: 1px solid var(--border);
}
.queue-item:last-child { border-bottom: none; }
.queue-num {
width: 22px; height: 22px; border-radius: 50%;
background: rgba(88,166,255,.12); border: 1px solid rgba(88,166,255,.25);
display: flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 700; color: var(--accent); flex-shrink: 0;
}
.queue-info { flex: 1; min-width: 0; }
.queue-name { font-size: 13px; font-weight: 500;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.queue-filter { font-size: 11px; color: var(--muted); margin-top: 2px;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.queue-del {
width: 26px; height: 26px; border: none; background: none;
color: var(--muted); cursor: pointer; border-radius: 4px;
display: flex; align-items: center; justify-content: center;
font-size: 13px; flex-shrink: 0; transition: all .12s;
}
.queue-del:hover { background: rgba(248,81,73,.15); color: var(--red); }
/* 响应式:手机适配(≤ 768px) */
@media (max-width: 768px) {
/* 顶部导航变两行,把 --hdr-h 调大供 formbar margin 跟随 */
@@ -629,7 +662,10 @@
}
.fb-field { padding: 0; width: 100%; border-right: none !important; min-width: 0; }
.fb-field-submit { padding: 0; }
.submit-row { flex-direction: column; gap: 8px; }
.btn-add-queue { width: 100%; padding: 11px; font-size: 13px; }
.btn-submit { width: 100%; justify-content: center; padding: 12px; font-size: 14px; }
.queue-filter { font-size: 10px; }
/* 验证下拉宽度自适应,避免溢出 */
.val-drop { width: auto; left: 12px; right: 12px; }
@@ -927,9 +963,14 @@
<!-- 提交 -->
<div class="fb-field fb-field-submit">
<button class="btn-submit" id="save-btn" onclick="saveAndRestart()">
🔄 保存并重启下载
</button>
<div class="submit-row">
<button class="btn-add-queue" id="add-queue-btn" onclick="addToQueue()">
加入队列
</button>
<button class="btn-submit" id="save-btn" onclick="saveAndRestart()" style="flex:1">
🔄 保存并重启下载
</button>
</div>
</div>
</div>
@@ -958,6 +999,18 @@
<div style="font-size:11px;color:var(--muted);flex-shrink:0;">当前任务</div>
</div>
<!-- 下载队列(待处理) -->
<div class="card" id="queue-card" style="display:none">
<div class="card-hd">
<span>
待处理队列
<span class="card-hd-badge" id="queue-count" style="margin-left:6px">0 个</span>
</span>
<button class="btn-clear" onclick="clearQueue()">清空</button>
</div>
<div class="card-bd" id="queue-list"></div>
</div>
<!-- 历史频道 -->
<div class="card">
<div class="hist-head" onclick="toggleHist()">
@@ -1211,41 +1264,147 @@
});
});
// ── 保存并重启 ──
function saveAndRestart() {
// ── 下载队列 ──
let downloadQueue = [];
function escapeHtml(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({
'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
})[c]);
}
function buildFilterFromForm(startDate, endDate) {
const custom = filterExpression || '';
if (custom) return custom;
if (!startDate) return '';
return 'message_date >= ' + startDate + ' 00:00:00' +
(endDate ? ' and message_date <= ' + endDate + ' 23:59:59' : '');
}
function renderQueue() {
const card = document.getElementById('queue-card');
const list = document.getElementById('queue-list');
const count = document.getElementById('queue-count');
if (!downloadQueue.length) { card.style.display = 'none'; return; }
card.style.display = '';
count.textContent = downloadQueue.length + ' 个';
list.innerHTML = downloadQueue.map((q, i) => `
<div class="queue-item">
<div class="queue-num">${i + 1}</div>
<div class="queue-info">
<div class="queue-name">${escapeHtml(q.chat_title || q.chat_id)}</div>
<div class="queue-filter" title="${escapeHtml(q.download_filter || '')}">${escapeHtml(q.download_filter || '无过滤条件')}</div>
</div>
<button class="queue-del" onclick="removeFromQueue(${i})" title="移除">✕</button>
</div>
`).join('');
}
function addToQueue() {
const channelId = parseChannelId(document.getElementById('channel-input').value);
const startDate = document.getElementById('start-date').value;
const endDate = document.getElementById('end-date').value;
if (!channelId) { toast('请输入有效的频道', 'err'); return; }
if (!startDate) { toast('请选择开始日期', 'err'); return; }
if (downloadQueue.some(q => q.chat_id === channelId)) {
toast('该频道已在队列中', 'err'); return;
}
const doSave = (chatTitle, chatType) => {
const btn = document.getElementById('save-btn');
btn.disabled = true; btn.innerHTML = '<span class="spin"></span> 重启中…';
const submittedFilter = filterExpression || '';
const builtFilter = startDate
? ('message_date >= ' + startDate + ' 00:00:00' + (endDate ? ' and message_date <= ' + endDate + ' 23:59:59' : ''))
: '';
fetch('/api/save_and_restart', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ chat_id:channelId, start_date:startDate, end_date:endDate, chat_title:chatTitle, chat_type:chatType||'', download_filter:submittedFilter })
}).then(r => r.json()).then(d => {
if (d.success) {
currentFilter = submittedFilter || builtFilter;
updateBannerFilter();
toast('✅ ' + d.message + '5秒后刷新…'); setTimeout(() => location.reload(), 5000);
} else { toast('失败: ' + (d.error||''), 'err'); btn.disabled = false; btn.textContent = '🔄 保存并重启下载'; }
}).catch(e => { toast('操作失败: ' + e.message, 'err'); btn.disabled = false; btn.textContent = '🔄 保存并重启下载'; });
const doAdd = (chatTitle, chatType) => {
downloadQueue.push({
chat_id: channelId,
chat_title: chatTitle || channelId,
chat_type: chatType || '',
download_filter: buildFilterFromForm(startDate, endDate),
start_date: startDate,
end_date: endDate,
});
renderQueue();
// 清空表单以便继续添加下一个频道
document.getElementById('channel-input').value = '';
validatedChannel = null;
const valBox = document.getElementById('val-result');
if (valBox) valBox.className = 'val-drop';
toast('已加入队列: ' + (chatTitle || channelId));
};
if (validatedChannel) {
doSave(validatedChannel.chat_title, validatedChannel.chat_type);
const btn = document.getElementById('add-queue-btn');
if (validatedChannel && validatedChannel.chat_id === channelId) {
doAdd(validatedChannel.chat_title, validatedChannel.chat_type);
} else {
const btn = document.getElementById('save-btn');
btn.disabled = true; btn.innerHTML = '<span class="spin"></span> 验证中…';
btn.disabled = true;
fetch('/api/validate_chat', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ chat_id: channelId }) })
.then(r => r.json()).then(d => {
if (d.valid) { showVal(d); validatedChannel = d; doSave(d.chat_title, d.chat_type); }
btn.disabled = false;
if (d.valid) { showVal(d); validatedChannel = d; doAdd(d.chat_title, d.chat_type); }
else { showVal(d); toast('频道验证失败', 'err'); }
}).catch(e => { btn.disabled = false; toast('验证失败: ' + e.message, 'err'); });
}
}
function removeFromQueue(idx) {
downloadQueue.splice(idx, 1);
renderQueue();
}
function clearQueue() {
if (!downloadQueue.length) return;
if (!confirm('确认清空队列里的 ' + downloadQueue.length + ' 个频道?')) return;
downloadQueue = [];
renderQueue();
}
// ── 保存并重启(队列 + 表单一起提交) ──
function saveAndRestart() {
const channelId = parseChannelId(document.getElementById('channel-input').value);
const startDate = document.getElementById('start-date').value;
const endDate = document.getElementById('end-date').value;
const hasForm = !!channelId;
const hasQueue = downloadQueue.length > 0;
if (!hasForm && !hasQueue) { toast('请输入频道或先加入队列', 'err'); return; }
if (hasForm && !startDate) { toast('请选择开始日期', 'err'); return; }
const btn = document.getElementById('save-btn');
btn.disabled = true; btn.innerHTML = '<span class="spin"></span> 重启中…';
const submitAll = (formItem) => {
const items = downloadQueue.slice();
if (formItem && !items.some(q => q.chat_id === formItem.chat_id)) items.push(formItem);
fetch('/api/save_and_restart_multi', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ items })
}).then(r => r.json()).then(d => {
if (d.success) {
toast('✅ ' + d.message + '5秒后刷新…');
setTimeout(() => location.reload(), 5000);
} else {
toast('失败: ' + (d.error||''), 'err');
btn.disabled = false; btn.textContent = '🔄 保存并重启下载';
}
}).catch(e => {
toast('操作失败: ' + e.message, 'err');
btn.disabled = false; btn.textContent = '🔄 保存并重启下载';
});
};
if (!hasForm) { submitAll(null); return; }
const buildFormItem = (chatTitle, chatType) => ({
chat_id: channelId,
chat_title: chatTitle || channelId,
chat_type: chatType || '',
download_filter: buildFilterFromForm(startDate, endDate),
start_date: startDate,
end_date: endDate,
});
if (validatedChannel && validatedChannel.chat_id === channelId) {
submitAll(buildFormItem(validatedChannel.chat_title, validatedChannel.chat_type));
} else {
fetch('/api/validate_chat', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ chat_id: channelId }) })
.then(r => r.json()).then(d => {
if (d.valid) { showVal(d); validatedChannel = d; submitAll(buildFormItem(d.chat_title, d.chat_type)); }
else { showVal(d); toast('频道验证失败', 'err'); btn.disabled = false; btn.textContent = '🔄 保存并重启下载'; }
}).catch(e => { toast('验证失败: ' + e.message, 'err'); btn.disabled = false; btn.textContent = '🔄 保存并重启下载'; });
}