+186
-27
@@ -580,6 +580,39 @@
|
|||||||
padding: 14px 24px; border-top: 1px solid var(--border); gap: 10px;
|
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) */
|
/* 响应式:手机适配(≤ 768px) */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* 顶部导航变两行,把 --hdr-h 调大供 formbar margin 跟随 */
|
/* 顶部导航变两行,把 --hdr-h 调大供 formbar margin 跟随 */
|
||||||
@@ -629,7 +662,10 @@
|
|||||||
}
|
}
|
||||||
.fb-field { padding: 0; width: 100%; border-right: none !important; min-width: 0; }
|
.fb-field { padding: 0; width: 100%; border-right: none !important; min-width: 0; }
|
||||||
.fb-field-submit { padding: 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; }
|
.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; }
|
.val-drop { width: auto; left: 12px; right: 12px; }
|
||||||
@@ -927,9 +963,14 @@
|
|||||||
|
|
||||||
<!-- 提交 -->
|
<!-- 提交 -->
|
||||||
<div class="fb-field fb-field-submit">
|
<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>
|
||||||
|
<button class="btn-submit" id="save-btn" onclick="saveAndRestart()" style="flex:1">
|
||||||
|
🔄 保存并重启下载
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -958,6 +999,18 @@
|
|||||||
<div style="font-size:11px;color:var(--muted);flex-shrink:0;">当前任务</div>
|
<div style="font-size:11px;color:var(--muted);flex-shrink:0;">当前任务</div>
|
||||||
</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="card">
|
||||||
<div class="hist-head" onclick="toggleHist()">
|
<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 channelId = parseChannelId(document.getElementById('channel-input').value);
|
||||||
const startDate = document.getElementById('start-date').value;
|
const startDate = document.getElementById('start-date').value;
|
||||||
const endDate = document.getElementById('end-date').value;
|
const endDate = document.getElementById('end-date').value;
|
||||||
if (!channelId) { toast('请输入有效的频道', 'err'); return; }
|
if (!channelId) { toast('请输入有效的频道', 'err'); return; }
|
||||||
if (!startDate) { toast('请选择开始日期', 'err'); return; }
|
if (!startDate) { toast('请选择开始日期', 'err'); return; }
|
||||||
|
if (downloadQueue.some(q => q.chat_id === channelId)) {
|
||||||
|
toast('该频道已在队列中', 'err'); return;
|
||||||
|
}
|
||||||
|
|
||||||
const doSave = (chatTitle, chatType) => {
|
const doAdd = (chatTitle, chatType) => {
|
||||||
const btn = document.getElementById('save-btn');
|
downloadQueue.push({
|
||||||
btn.disabled = true; btn.innerHTML = '<span class="spin"></span> 重启中…';
|
chat_id: channelId,
|
||||||
const submittedFilter = filterExpression || '';
|
chat_title: chatTitle || channelId,
|
||||||
const builtFilter = startDate
|
chat_type: chatType || '',
|
||||||
? ('message_date >= ' + startDate + ' 00:00:00' + (endDate ? ' and message_date <= ' + endDate + ' 23:59:59' : ''))
|
download_filter: buildFilterFromForm(startDate, endDate),
|
||||||
: '';
|
start_date: startDate,
|
||||||
fetch('/api/save_and_restart', {
|
end_date: endDate,
|
||||||
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 })
|
renderQueue();
|
||||||
}).then(r => r.json()).then(d => {
|
// 清空表单以便继续添加下一个频道
|
||||||
if (d.success) {
|
document.getElementById('channel-input').value = '';
|
||||||
currentFilter = submittedFilter || builtFilter;
|
validatedChannel = null;
|
||||||
updateBannerFilter();
|
const valBox = document.getElementById('val-result');
|
||||||
toast('✅ ' + d.message + ',5秒后刷新…'); setTimeout(() => location.reload(), 5000);
|
if (valBox) valBox.className = 'val-drop';
|
||||||
} else { toast('失败: ' + (d.error||''), 'err'); btn.disabled = false; btn.textContent = '🔄 保存并重启下载'; }
|
toast('已加入队列: ' + (chatTitle || channelId));
|
||||||
}).catch(e => { toast('操作失败: ' + e.message, 'err'); btn.disabled = false; btn.textContent = '🔄 保存并重启下载'; });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (validatedChannel) {
|
const btn = document.getElementById('add-queue-btn');
|
||||||
doSave(validatedChannel.chat_title, validatedChannel.chat_type);
|
if (validatedChannel && validatedChannel.chat_id === channelId) {
|
||||||
|
doAdd(validatedChannel.chat_title, validatedChannel.chat_type);
|
||||||
} else {
|
} else {
|
||||||
const btn = document.getElementById('save-btn');
|
btn.disabled = true;
|
||||||
btn.disabled = true; btn.innerHTML = '<span class="spin"></span> 验证中…';
|
|
||||||
fetch('/api/validate_chat', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ chat_id: channelId }) })
|
fetch('/api/validate_chat', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ chat_id: channelId }) })
|
||||||
.then(r => r.json()).then(d => {
|
.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 = '🔄 保存并重启下载'; }
|
else { showVal(d); toast('频道验证失败', 'err'); btn.disabled = false; btn.textContent = '🔄 保存并重启下载'; }
|
||||||
}).catch(e => { toast('验证失败: ' + e.message, '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)}
|
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:
|
async def _validate_chat(chat_id: str) -> dict:
|
||||||
"""
|
"""
|
||||||
Validate chat/channel and get its info.
|
Validate chat/channel and get its info.
|
||||||
@@ -804,6 +856,62 @@ def api_save_and_restart():
|
|||||||
return jsonify({"success": False, "error": str(e)})
|
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"])
|
@_flask_app.route("/api/start_download", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def api_start_download():
|
def api_start_download():
|
||||||
|
|||||||
Reference in New Issue
Block a user