Files
yuming cf40343c51
部署到群晖 / deploy (push) Failing after 10m45s
初始化 telegram-downloader 并接入群晖 CI/CD
2026-04-22 21:29:03 +08:00

671 lines
31 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>设置 — Telegram 下载器</title>
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #0d1117;
--surface: #161b22;
--surface2: #1c2128;
--border: #30363d;
--accent: #58a6ff;
--green: #3fb950;
--orange: #d29922;
--red: #f85149;
--text: #e6edf3;
--muted: #8b949e;
--hdr-h: 52px;
}
html, body { height: 100%; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg); color: var(--text); font-size: 14px; line-height: 1.5; }
/* ── Header ── */
.hdr {
position: fixed; top: 0; left: 0; right: 0; height: var(--hdr-h);
background: var(--surface); border-bottom: 1px solid var(--border);
display: flex; align-items: center; padding: 0 20px; gap: 12px; z-index: 200;
}
.hdr-back {
display: flex; align-items: center; gap: 5px; color: var(--muted);
text-decoration: none; font-size: 13px; padding: 4px 10px;
border: 1px solid var(--border); border-radius: 6px; transition: all .15s;
}
.hdr-back:hover { color: var(--accent); border-color: var(--accent); }
.hdr-title { font-size: 15px; font-weight: 700; color: var(--accent); }
/* ── Main Layout ── */
.main { margin-top: var(--hdr-h); display: flex; height: calc(100vh - var(--hdr-h)); }
/* ── Sidebar Tabs ── */
.sidebar {
width: 180px; flex-shrink: 0;
background: var(--surface); border-right: 1px solid var(--border);
padding: 16px 8px; display: flex; flex-direction: column; gap: 4px;
}
.stab {
display: flex; align-items: center; gap: 8px;
padding: 9px 12px; border-radius: 7px; cursor: pointer;
font-size: 13px; color: var(--muted); border: none; background: none;
text-align: left; transition: all .12s; width: 100%;
}
.stab:hover { background: rgba(88,166,255,.07); color: var(--text); }
.stab.active { background: rgba(88,166,255,.12); color: var(--accent); font-weight: 600; }
.stab-icon { font-size: 15px; flex-shrink: 0; }
/* ── Content ── */
.content { flex: 1; overflow-y: auto; padding: 24px; }
.tab-panel { display: none; max-width: 800px; }
.tab-panel.active { display: block; }
/* ── Card ── */
.card {
background: var(--surface); border: 1px solid var(--border);
border-radius: 8px; margin-bottom: 16px;
}
.card-hd {
padding: 12px 16px; border-bottom: 1px solid var(--border);
font-size: 13px; font-weight: 600; display: flex; align-items: center;
justify-content: space-between;
}
.card-bd { padding: 16px; }
/* ── Form Controls ── */
.field { margin-bottom: 14px; }
.field:last-child { margin-bottom: 0; }
.field-label { display: block; font-size: 11px; color: var(--muted);
margin-bottom: 5px; font-weight: 600; text-transform: uppercase; letter-spacing: .4px; }
.field-hint { font-size: 11px; color: var(--muted); margin-top: 4px; }
.field-input {
width: 100%; padding: 7px 10px; background: var(--bg);
border: 1px solid var(--border); border-radius: 6px;
color: var(--text); font-size: 13px; transition: border-color .15s;
}
.field-input:focus { outline: none; border-color: var(--accent); }
.field-input-sm { width: 120px; }
select.field-input { cursor: pointer; }
/* ── Buttons ── */
.btn {
padding: 7px 16px; border-radius: 6px; font-size: 13px; font-weight: 500;
cursor: pointer; border: none; display: inline-flex; align-items: center;
gap: 5px; transition: opacity .15s; white-space: nowrap;
}
.btn:disabled { opacity: .4; cursor: not-allowed; }
.btn-primary { background: var(--accent); color: #0d1117; }
.btn-primary:hover:not(:disabled) { opacity: .85; }
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--muted); }
.btn-outline:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.btn-danger { background: transparent; border: 1px solid rgba(248,81,73,.4); color: var(--red); }
.btn-danger:hover:not(:disabled) { background: rgba(248,81,73,.1); }
.btn-sm { padding: 4px 10px; font-size: 12px; }
/* ── Badges ── */
.badge {
display: inline-flex; align-items: center; padding: 2px 7px;
border-radius: 4px; font-size: 11px; font-weight: 600;
}
.badge-green { background: rgba(63,185,80,.15); color: var(--green); }
.badge-orange { background: rgba(210,153,34,.15); color: var(--orange); }
.badge-red { background: rgba(248,81,73,.15); color: var(--red); }
.badge-blue { background: rgba(88,166,255,.15); color: var(--accent); }
.badge-muted { background: rgba(139,148,158,.12); color: var(--muted); }
/* ── Log List ── */
.log-toolbar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; flex-wrap: wrap; }
.log-list {
font-family: 'SFMono-Regular', Consolas, monospace; font-size: 12px;
line-height: 1.6; overflow-x: auto;
}
.log-line { padding: 2px 0; border-bottom: 1px solid rgba(48,54,61,.5); white-space: pre-wrap; word-break: break-all; }
.log-line:last-child { border-bottom: none; }
.log-err { color: var(--red); }
.log-warn { color: var(--orange); }
.log-info { color: var(--text); }
.log-debug { color: var(--muted); }
.log-empty { text-align: center; padding: 24px; color: var(--muted); font-size: 13px; }
/* ── Table ── */
.tbl-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 12px; }
th { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border);
color: var(--muted); font-weight: 600; font-size: 11px; white-space: nowrap; }
td { padding: 7px 10px; border-bottom: 1px solid rgba(48,54,61,.6); }
tr:last-child td { border-bottom: none; }
tr:hover td { background: rgba(88,166,255,.04); }
.td-truncate { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ── Pagination ── */
.pager { display: flex; align-items: center; gap: 6px; margin-top: 14px;
font-size: 12px; color: var(--muted); }
.pager-btn {
padding: 4px 10px; border-radius: 5px; background: transparent;
border: 1px solid var(--border); color: var(--muted);
cursor: pointer; font-size: 12px; transition: all .12s;
}
.pager-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.pager-btn:disabled { opacity: .4; cursor: default; }
.pager-info { flex: 1; text-align: center; }
.pager-jump { display: flex; align-items: center; gap: 5px; }
.pager-jump input { width: 54px; padding: 3px 7px; background: var(--bg);
border: 1px solid var(--border); border-radius: 5px; color: var(--text);
font-size: 12px; text-align: center; }
.pager-jump input:focus { outline: none; border-color: var(--accent); }
/* ── DB Search Bar ── */
.search-bar { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
.search-bar .field-input { width: auto; flex: 1; min-width: 120px; }
.search-bar select.field-input { width: auto; }
/* ── Status/Session ── */
.info-row { display: flex; justify-content: space-between; align-items: center;
padding: 10px 0; border-bottom: 1px solid var(--border); }
.info-row:last-child { border-bottom: none; }
.info-k { font-size: 12px; color: var(--muted); }
.info-v { font-size: 12px; }
/* ── Proxy Fields ── */
.proxy-grid { display: grid; grid-template-columns: 1fr 1fr 100px; gap: 10px; }
/* ── Toast ── */
.toasts { position: fixed; top: 60px; right: 14px; display: flex;
flex-direction: column; gap: 7px; z-index: 999; pointer-events: none; }
.toast { padding: 9px 16px; border-radius: 7px; font-size: 13px; animation: tin .2s ease; }
.toast-ok { background: rgba(63,185,80,.92); color: #0d1117; }
.toast-err { background: rgba(248,81,73,.92); color: #fff; }
@keyframes tin { from { opacity:0; transform:translateX(16px); } to { opacity:1; transform:none; } }
/* ── Spinner ── */
.spin { display: inline-block; width: 11px; height: 11px;
border: 2px solid rgba(255,255,255,.3); border-radius: 50%;
border-top-color: currentColor; animation: sp 1s linear infinite; }
@keyframes sp { to { transform: rotate(360deg); } }
/* ── Confirm Modal ── */
.modal-mask { display: none; position: fixed; inset: 0;
background: rgba(0,0,0,.7); z-index: 500;
align-items: center; justify-content: center; }
.modal-mask.on { display: flex; }
.modal-box { background: var(--surface); border: 1px solid var(--border);
border-radius: 10px; padding: 24px; max-width: 380px; width: 90%; }
.modal-title { font-size: 15px; font-weight: 700; margin-bottom: 8px; }
.modal-body { font-size: 13px; color: var(--muted); margin-bottom: 20px; line-height: 1.6; }
.modal-foot { display: flex; gap: 8px; justify-content: flex-end; }
</style>
</head>
<body>
<!-- Header -->
<header class="hdr">
<a href="/" class="hdr-back">← 返回主页</a>
<span style="color:var(--border);">|</span>
<div class="hdr-title">⚙ 设置</div>
</header>
<div class="toasts" id="toasts"></div>
<!-- 确认弹窗 -->
<div class="modal-mask" id="confirm-modal">
<div class="modal-box">
<div class="modal-title" id="modal-title">确认操作</div>
<div class="modal-body" id="modal-body"></div>
<div class="modal-foot">
<button class="btn btn-outline btn-sm" onclick="closeConfirm()">取消</button>
<button class="btn btn-danger btn-sm" id="modal-ok-btn" onclick="doConfirm()">确认</button>
</div>
</div>
</div>
<div class="main">
<!-- Sidebar -->
<nav class="sidebar">
<button class="stab active" onclick="switchTab('general')"><span class="stab-icon"></span>常规设置</button>
<button class="stab" onclick="switchTab('logs')"><span class="stab-icon">📋</span>运行日志</button>
<button class="stab" onclick="switchTab('db')"><span class="stab-icon">🗄</span>下载记录</button>
<button class="stab" onclick="switchTab('account')"><span class="stab-icon">👤</span>账户管理</button>
</nav>
<!-- Content -->
<div class="content">
<!-- Tab: 常规设置 -->
<div class="tab-panel active" id="tab-general">
<div class="card">
<div class="card-hd">下载设置</div>
<div class="card-bd">
<div class="field">
<label class="field-label">并发下载数量</label>
<input class="field-input field-input-sm" type="number" id="g-max-task" min="1" max="20" value="5">
<div class="field-hint">同时下载的文件数,范围 1–20,修改后自动重启生效</div>
</div>
<div class="field">
<label class="field-label">日志级别</label>
<select class="field-input field-input-sm" id="g-log-level">
<option value="INFO">INFO(推荐)</option>
<option value="DEBUG">DEBUG(详细)</option>
<option value="WARNING">WARNING(仅警告)</option>
</select>
<div class="field-hint">修改后重启生效</div>
</div>
<div class="field">
<label class="field-label">文件保存路径</label>
<input class="field-input" type="text" id="g-save-path" placeholder="/path/to/downloads">
</div>
</div>
</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-primary" id="g-save-btn" onclick="saveGeneral()">💾 保存并重启</button>
</div>
</div>
<!-- Tab: 运行日志 -->
<div class="tab-panel" id="tab-logs">
<div class="card">
<div class="card-hd">
<span>运行日志</span>
<span id="log-total-badge" style="font-size:11px;color:var(--muted);"></span>
</div>
<div class="card-bd">
<div class="log-toolbar">
<select class="field-input" style="width:130px;" id="log-level-filter">
<option value="">全部级别</option>
<option value="INFO">INFO</option>
<option value="WARNING">WARNING</option>
<option value="ERROR">ERROR</option>
<option value="DEBUG">DEBUG</option>
</select>
<input class="field-input" style="flex:1;min-width:140px;" type="text"
id="log-keyword" placeholder="关键词搜索…" onkeydown="if(event.key==='Enter')loadLogs(1)">
<button class="btn btn-outline btn-sm" onclick="loadLogs(1)">🔍 搜索</button>
<button class="btn btn-outline btn-sm" onclick="loadLogs(logPage)">↺ 刷新</button>
</div>
<div id="log-list" class="log-list"><div class="log-empty">加载中…</div></div>
<div class="pager" id="log-pager" style="display:none;">
<button class="pager-btn" id="log-prev" onclick="loadLogs(logPage-1)"> 上一页</button>
<div class="pager-info" id="log-pager-info"></div>
<div class="pager-jump">
跳转 <input type="number" id="log-jump-input" min="1" onkeydown="if(event.key==='Enter')loadLogs(+this.value)">
</div>
<button class="pager-btn" id="log-next" onclick="loadLogs(logPage+1)">下一页 </button>
</div>
</div>
</div>
</div>
<!-- Tab: 下载记录 -->
<div class="tab-panel" id="tab-db">
<div class="card">
<div class="card-hd">
<span>下载记录</span>
<span id="db-total-badge" style="font-size:11px;color:var(--muted);"></span>
</div>
<div class="card-bd">
<div class="search-bar">
<input class="field-input" type="text" id="db-chat" placeholder="频道ID/名称">
<input class="field-input" type="text" id="db-filename" placeholder="文件名">
<select class="field-input" id="db-status">
<option value="">全部状态</option>
<option value="success">success</option>
<option value="skip">skip</option>
</select>
<select class="field-input" id="db-mediatype">
<option value="">全部类型</option>
<option value="audio">audio</option>
<option value="video">video</option>
<option value="photo">photo</option>
<option value="document">document</option>
</select>
<input class="field-input" type="date" id="db-date-from" title="开始日期">
<input class="field-input" type="date" id="db-date-to" title="结束日期">
<button class="btn btn-primary btn-sm" onclick="loadDb(1)">搜索</button>
<button class="btn btn-outline btn-sm" onclick="clearDbSearch()">重置</button>
</div>
<div class="tbl-wrap">
<table>
<thead>
<tr>
<th>ID</th><th>频道</th><th>消息ID</th><th>文件名</th>
<th>大小</th><th>类型</th><th>时间</th><th>状态</th><th>操作</th>
</tr>
</thead>
<tbody id="db-tbody"><tr><td colspan="9" style="text-align:center;padding:20px;color:var(--muted);">加载中…</td></tr></tbody>
</table>
</div>
<div class="pager" id="db-pager" style="display:none;">
<button class="pager-btn" id="db-prev" onclick="loadDb(dbPage-1)"> 上一页</button>
<div class="pager-info" id="db-pager-info"></div>
<div class="pager-jump">
跳转 <input type="number" id="db-jump-input" min="1" onkeydown="if(event.key==='Enter')loadDb(+this.value)">
</div>
<button class="pager-btn" id="db-next" onclick="loadDb(dbPage+1)">下一页 </button>
</div>
</div>
</div>
</div>
<!-- Tab: 账户管理 -->
<div class="tab-panel" id="tab-account">
<div class="card">
<div class="card-hd">Session 状态</div>
<div class="card-bd" id="session-info">
<div style="color:var(--muted);font-size:13px;">检测中…</div>
</div>
</div>
<div class="card">
<div class="card-hd">代理设置</div>
<div class="card-bd">
<div class="field">
<label class="field-label">代理类型</label>
<select class="field-input field-input-sm" id="p-scheme">
<option value="socks5">SOCKS5</option>
<option value="http">HTTP</option>
</select>
</div>
<div class="proxy-grid">
<div class="field">
<label class="field-label">地址</label>
<input class="field-input" type="text" id="p-host" placeholder="192.168.1.1">
</div>
<div class="field">
<label class="field-label">端口</label>
<input class="field-input" type="number" id="p-port" placeholder="7890">
</div>
<div class="field">
<label class="field-label"> </label>
<button class="btn btn-outline btn-sm" onclick="testProxy()" id="p-test-btn">🔌 测试</button>
<span id="p-test-result" style="font-size:11px;display:block;margin-top:4px;"></span>
</div>
</div>
<div class="field-hint" style="margin-bottom:12px;">留空地址则不使用代理。修改后自动重启。</div>
<button class="btn btn-primary btn-sm" id="p-save-btn" onclick="saveProxy()">💾 保存代理并重启</button>
</div>
</div>
<div class="card">
<div class="card-hd">重新配置账户</div>
<div class="card-bd">
<p style="font-size:13px;color:var(--muted);margin-bottom:14px;line-height:1.7;">
点击下方按钮将删除当前登录 session,服务重启后需在终端重新运行登录命令完成 Telegram 账号验证。<br>
<span style="color:var(--orange);">⚠ 操作不可撤销,当前下载任务将被中断。</span>
</p>
<button class="btn btn-danger" onclick="confirmClearSession()">🔑 清除 Session 并重新登录</button>
</div>
</div>
</div>
</div><!-- /content -->
</div><!-- /main -->
<script>
// ── Toast ──
function toast(msg, type = 'ok') {
const el = document.createElement('div');
el.className = 'toast toast-' + type;
el.textContent = msg;
document.getElementById('toasts').appendChild(el);
setTimeout(() => el.remove(), 3500);
}
function fmtBytes(n) {
if (!n) return '—';
n = +n;
if (n < 1024) return n + ' B';
if (n < 1024**2) return (n/1024).toFixed(1) + ' KB';
if (n < 1024**3) return (n/1024**2).toFixed(1) + ' MB';
return (n/1024**3).toFixed(2) + ' GB';
}
// ── Tab 切换 ──
const TAB_INIT = { logs: false, db: false, account: false };
function switchTab(name) {
document.querySelectorAll('.stab').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
document.querySelector(`.stab[onclick*="${name}"]`).classList.add('active');
document.getElementById('tab-' + name).classList.add('active');
if (!TAB_INIT[name]) {
TAB_INIT[name] = true;
if (name === 'logs') loadLogs(1);
if (name === 'db') loadDb(1);
if (name === 'account') loadAccountInfo();
}
}
// ════ 常规设置 ════
function loadGeneral() {
fetch('/api/settings/general').then(r => r.json()).then(d => {
document.getElementById('g-max-task').value = d.max_download_task || 5;
document.getElementById('g-log-level').value = d.log_level || 'INFO';
document.getElementById('g-save-path').value = d.save_path || '';
}).catch(() => {});
}
function saveGeneral() {
const btn = document.getElementById('g-save-btn');
btn.disabled = true; btn.innerHTML = '<span class="spin"></span> 保存中…';
const body = {
max_download_task: +document.getElementById('g-max-task').value,
log_level: document.getElementById('g-log-level').value,
save_path: document.getElementById('g-save-path').value.trim(),
};
fetch('/api/settings/general', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify(body)
}).then(r => r.json()).then(d => {
if (d.success) { toast('已保存,5秒后重启…'); setTimeout(() => location.href = '/', 5000); }
else { toast(d.error || '保存失败', 'err'); btn.disabled = false; btn.textContent = '💾 保存并重启'; }
}).catch(() => { toast('请求失败', 'err'); btn.disabled = false; btn.textContent = '💾 保存并重启'; });
}
// ════ 运行日志 ════
let logPage = 1, logPages = 1;
function loadLogs(page) {
page = Math.max(1, Math.min(page, logPages));
logPage = page;
const level = document.getElementById('log-level-filter').value;
const kw = document.getElementById('log-keyword').value.trim();
const url = `/api/settings/logs?page=${page}&page_size=100&level=${encodeURIComponent(level)}&keyword=${encodeURIComponent(kw)}`;
document.getElementById('log-list').innerHTML = '<div class="log-empty">加载中…</div>';
fetch(url).then(r => r.json()).then(d => {
logPages = d.pages || 1;
document.getElementById('log-total-badge').textContent = `${d.total}`;
if (!d.lines || !d.lines.length) {
document.getElementById('log-list').innerHTML = '<div class="log-empty">暂无日志</div>';
document.getElementById('log-pager').style.display = 'none';
return;
}
document.getElementById('log-list').innerHTML = d.lines.map(line => {
let cls = 'log-info';
if (line.includes('| ERROR') || line.includes('|ERROR')) cls = 'log-err';
else if (line.includes('| WARNING') || line.includes('|WARNING')) cls = 'log-warn';
else if (line.includes('| DEBUG') || line.includes('|DEBUG')) cls = 'log-debug';
const escaped = line.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
return `<div class="log-line ${cls}">${escaped}</div>`;
}).join('');
const pager = document.getElementById('log-pager');
pager.style.display = 'flex';
document.getElementById('log-pager-info').textContent = `${logPage} / ${logPages}`;
document.getElementById('log-prev').disabled = logPage <= 1;
document.getElementById('log-next').disabled = logPage >= logPages;
document.getElementById('log-jump-input').value = logPage;
}).catch(() => { document.getElementById('log-list').innerHTML = '<div class="log-empty">加载失败</div>'; });
}
// ════ 下载记录 ════
let dbPage = 1, dbPages = 1;
function loadDb(page) {
page = Math.max(1, Math.min(page, dbPages));
dbPage = page;
const params = new URLSearchParams({
page, page_size: 50,
chat_id: document.getElementById('db-chat').value.trim(),
file_name: document.getElementById('db-filename').value.trim(),
status: document.getElementById('db-status').value,
media_type: document.getElementById('db-mediatype').value,
date_from: document.getElementById('db-date-from').value,
date_to: document.getElementById('db-date-to').value,
});
document.getElementById('db-tbody').innerHTML = '<tr><td colspan="9" style="text-align:center;padding:16px;color:var(--muted);">加载中…</td></tr>';
fetch('/api/settings/db_records?' + params).then(r => r.json()).then(d => {
dbPages = d.pages || 1;
document.getElementById('db-total-badge').textContent = `${d.total}`;
if (!d.records || !d.records.length) {
document.getElementById('db-tbody').innerHTML = '<tr><td colspan="9" style="text-align:center;padding:20px;color:var(--muted);">暂无记录</td></tr>';
document.getElementById('db-pager').style.display = 'none';
return;
}
document.getElementById('db-tbody').innerHTML = d.records.map(r => {
const statusBadge = r.status === 'success'
? `<span class="badge badge-green">成功</span>`
: r.status === 'skip'
? `<span class="badge badge-orange">跳过</span>`
: `<span class="badge badge-muted">${r.status||'—'}</span>`;
const undoBtn = r.status === 'skip'
? `<button class="btn btn-outline btn-sm" onclick="undoSkip('${r.chat_id}',${r.message_id},this)">撤销</button>`
: '';
return `<tr>
<td>${r.id}</td>
<td class="td-truncate" title="${r.chat_title||r.chat_id}">${r.chat_title||r.chat_id||'—'}</td>
<td>${r.message_id}</td>
<td class="td-truncate" title="${r.file_name||''}">${r.file_name||'—'}</td>
<td style="white-space:nowrap">${fmtBytes(r.file_size)}</td>
<td>${r.media_type||'—'}</td>
<td style="white-space:nowrap;font-size:11px;">${(r.download_time||'').slice(0,16)}</td>
<td>${statusBadge}</td>
<td>${undoBtn}</td>
</tr>`;
}).join('');
const pager = document.getElementById('db-pager');
pager.style.display = 'flex';
document.getElementById('db-pager-info').textContent = `${dbPage} / ${dbPages}`;
document.getElementById('db-prev').disabled = dbPage <= 1;
document.getElementById('db-next').disabled = dbPage >= dbPages;
document.getElementById('db-jump-input').value = dbPage;
}).catch(() => { document.getElementById('db-tbody').innerHTML = '<tr><td colspan="9" style="text-align:center;padding:20px;color:var(--red);">加载失败</td></tr>'; });
}
function clearDbSearch() {
['db-chat','db-filename'].forEach(id => document.getElementById(id).value = '');
['db-status','db-mediatype'].forEach(id => document.getElementById(id).value = '');
['db-date-from','db-date-to'].forEach(id => document.getElementById(id).value = '');
loadDb(1);
}
function undoSkip(chatId, msgId, btn) {
btn.disabled = true;
fetch('/api/undo_skip', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({chat_id: chatId, message_id: +msgId})
}).then(r => r.json()).then(d => {
if (d.success) { toast('已撤销跳过'); loadDb(dbPage); }
else { toast(d.error || '撤销失败', 'err'); btn.disabled = false; }
}).catch(() => { toast('请求失败', 'err'); btn.disabled = false; });
}
// ════ 账户管理 ════
function loadAccountInfo() {
fetch('/api/settings/general').then(r => r.json()).then(d => {
const p = d.proxy || {};
document.getElementById('p-scheme').value = p.scheme || 'socks5';
document.getElementById('p-host').value = p.hostname || '';
document.getElementById('p-port').value = p.port || '';
}).catch(() => {});
fetch('/api/setup_status').then(r => r.json()).then(s => {
const el = document.getElementById('session-info');
el.innerHTML = `
<div class="info-row">
<span class="info-k">Session 文件</span>
<span class="info-v">${s.has_session
? '<span class="badge badge-green">✓ 已登录</span>'
: '<span class="badge badge-red">✗ 未登录</span>'}</span>
</div>
<div class="info-row">
<span class="info-k">API 凭证</span>
<span class="info-v">${s.has_api_credentials
? '<span class="badge badge-green">✓ 已配置</span>'
: '<span class="badge badge-red">✗ 未配置</span>'}</span>
</div>`;
}).catch(() => {});
}
function testProxy() {
const btn = document.getElementById('p-test-btn');
const res = document.getElementById('p-test-result');
btn.disabled = true; btn.innerHTML = '<span class="spin"></span>';
res.textContent = '';
fetch('/api/test_proxy', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({
scheme: document.getElementById('p-scheme').value,
hostname: document.getElementById('p-host').value.trim(),
port: +document.getElementById('p-port').value || 0,
})
}).then(r => r.json()).then(d => {
res.style.color = d.success ? 'var(--green)' : 'var(--red)';
res.textContent = d.success ? ('✓ ' + d.message) : ('✗ ' + d.error);
}).catch(e => { res.style.color = 'var(--red)'; res.textContent = '✗ ' + e.message; })
.finally(() => { btn.disabled = false; btn.textContent = '🔌 测试'; });
}
function saveProxy() {
const btn = document.getElementById('p-save-btn');
btn.disabled = true; btn.innerHTML = '<span class="spin"></span> 保存中…';
fetch('/api/settings/general', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({
proxy: {
scheme: document.getElementById('p-scheme').value,
hostname: document.getElementById('p-host').value.trim(),
port: +document.getElementById('p-port').value || 7890,
}
})
}).then(r => r.json()).then(d => {
if (d.success) { toast('代理已保存,5秒后重启…'); setTimeout(() => location.href = '/', 5000); }
else { toast(d.error || '保存失败', 'err'); btn.disabled = false; btn.textContent = '💾 保存代理并重启'; }
}).catch(() => { toast('请求失败', 'err'); btn.disabled = false; btn.textContent = '💾 保存代理并重启'; });
}
// ── 确认弹窗 ──
let _confirmCallback = null;
function showConfirm(title, body, cb) {
document.getElementById('modal-title').textContent = title;
document.getElementById('modal-body').textContent = body;
_confirmCallback = cb;
document.getElementById('confirm-modal').classList.add('on');
}
function closeConfirm() {
document.getElementById('confirm-modal').classList.remove('on');
_confirmCallback = null;
}
function doConfirm() {
closeConfirm();
if (_confirmCallback) _confirmCallback();
}
function confirmClearSession() {
showConfirm(
'确认清除 Session',
'此操作将删除当前登录凭证并重启服务。重启后需在终端运行登录命令重新完成 Telegram 账号验证。',
clearSession
);
}
function clearSession() {
fetch('/api/settings/clear_session', { method: 'POST' })
.then(r => r.json()).then(d => {
if (d.success) { toast('Session 已清除,服务即将重启…'); setTimeout(() => location.href = '/', 5000); }
else toast(d.error || '操作失败', 'err');
}).catch(() => toast('请求失败', 'err'));
}
// ── 初始化 ──
loadGeneral();
</script>
</body>
</html>