671 lines
31 KiB
HTML
671 lines
31 KiB
HTML
<!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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
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>
|