+184
-25
@@ -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,10 +963,15 @@
|
||||
|
||||
<!-- 提交 -->
|
||||
<div class="fb-field fb-field-submit">
|
||||
<button class="btn-submit" id="save-btn" onclick="saveAndRestart()">
|
||||
<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 => ({
|
||||
'&':'&','<':'<','>':'>','"':'"',"'":'''
|
||||
})[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 = '🔄 保存并重启下载'; });
|
||||
}
|
||||
|
||||
+108
@@ -646,6 +646,58 @@ def _update_chat_config(chat_id: str, download_filter: str, update_memory: bool
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
def _update_chat_configs_multi(items: list, update_memory: bool = True) -> dict:
|
||||
"""
|
||||
多频道版本:把 items 数组写入 config["chat"],完整覆盖原数组。
|
||||
每个 item 形如 {"chat_id": "xxx", "download_filter": "xxx"}。
|
||||
同步更新内存里的 _app.chat_download_config。
|
||||
"""
|
||||
global _app
|
||||
if not items:
|
||||
return {"success": False, "error": "队列为空"}
|
||||
|
||||
try:
|
||||
yaml = YAML()
|
||||
yaml.preserve_quotes = True
|
||||
with open(CONFIG_FILE_PATH, "r", encoding="utf-8") as f:
|
||||
config = yaml.load(f)
|
||||
|
||||
# 按队列顺序重建 chat 数组
|
||||
new_chat_list = []
|
||||
for item in items:
|
||||
chat_id = str(item.get("chat_id", "")).strip()
|
||||
download_filter = str(item.get("download_filter", "") or "").strip()
|
||||
if not chat_id:
|
||||
continue
|
||||
entry = {"chat_id": chat_id, "last_read_message_id": 0}
|
||||
if download_filter:
|
||||
entry["download_filter"] = download_filter
|
||||
new_chat_list.append(entry)
|
||||
|
||||
if not new_chat_list:
|
||||
return {"success": False, "error": "队列中没有有效的频道"}
|
||||
|
||||
config["chat"] = new_chat_list
|
||||
|
||||
with open(CONFIG_FILE_PATH, "w", encoding="utf-8") as f:
|
||||
yaml.dump(config, f)
|
||||
|
||||
# 同步内存配置
|
||||
if update_memory and _app:
|
||||
_app.config["chat"] = new_chat_list
|
||||
_app.chat_download_config.clear()
|
||||
for entry in new_chat_list:
|
||||
c = ChatDownloadConfig()
|
||||
c.last_read_message_id = 0
|
||||
c.download_filter = entry.get("download_filter", "")
|
||||
_app.chat_download_config[entry["chat_id"]] = c
|
||||
|
||||
return {"success": True, "count": len(new_chat_list)}
|
||||
except Exception as e:
|
||||
logger.exception(f"Error updating multi chat config: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
async def _validate_chat(chat_id: str) -> dict:
|
||||
"""
|
||||
Validate chat/channel and get its info.
|
||||
@@ -804,6 +856,62 @@ def api_save_and_restart():
|
||||
return jsonify({"success": False, "error": str(e)})
|
||||
|
||||
|
||||
@_flask_app.route("/api/save_and_restart_multi", methods=["POST"])
|
||||
@login_required
|
||||
def api_save_and_restart_multi():
|
||||
"""多频道版本的保存并重启。接收 items 数组,每项含 chat_id + download_filter。"""
|
||||
global _app
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
items = data.get("items", [])
|
||||
if not isinstance(items, list) or not items:
|
||||
return jsonify({"success": False, "error": "队列为空"})
|
||||
|
||||
# 规范化 items:每项必须有 chat_id;download_filter 可由 start_date/end_date 构建
|
||||
normalized = []
|
||||
for it in items:
|
||||
cid = str(it.get("chat_id", "") or "").strip()
|
||||
if not cid:
|
||||
continue
|
||||
flt = str(it.get("download_filter", "") or "").strip()
|
||||
if not flt:
|
||||
sd = str(it.get("start_date", "") or "").strip()
|
||||
ed = str(it.get("end_date", "") or "").strip()
|
||||
if sd:
|
||||
flt = _build_download_filter(sd, ed)
|
||||
normalized.append({
|
||||
"chat_id": cid,
|
||||
"download_filter": flt,
|
||||
"chat_title": it.get("chat_title", "") or "",
|
||||
"chat_type": it.get("chat_type", "") or "",
|
||||
})
|
||||
|
||||
if not normalized:
|
||||
return jsonify({"success": False, "error": "没有有效的频道"})
|
||||
|
||||
result = _update_chat_configs_multi(normalized, update_memory=True)
|
||||
if not result.get("success"):
|
||||
return jsonify(result)
|
||||
|
||||
# 历史记录更新
|
||||
for it in normalized:
|
||||
_add_to_channel_history(it["chat_id"], it["chat_title"], it["chat_type"])
|
||||
|
||||
# 触发重启
|
||||
if _app:
|
||||
_app.restart_program = True
|
||||
logger.info(f"Restart flag set, {len(normalized)} chats queued")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"count": len(normalized),
|
||||
"message": f"已保存 {len(normalized)} 个频道任务,正在重启..."
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in api_save_and_restart_multi: {e}")
|
||||
return jsonify({"success": False, "error": str(e)})
|
||||
|
||||
|
||||
@_flask_app.route("/api/start_download", methods=["POST"])
|
||||
@login_required
|
||||
def api_start_download():
|
||||
|
||||
Reference in New Issue
Block a user