first commit
This commit is contained in:
41 files changed
+4964
No files matched your search
@@ -0,0 +1,79 @@
|
||||
'use strict';
|
||||
const $ = id => document.getElementById(id);
|
||||
let token = '', running = false, starting = false, loadingChannels = false, loadedKey = '', lastLog = -1;
|
||||
const labels = {idle:'等待开始',running:'下载中',completed:'已完成',partial:'未达到目标',cancelled:'已停止',error:'任务中断'};
|
||||
const key = () => $('key').value.trim();
|
||||
function controls() {
|
||||
const busy = running || starting;
|
||||
$('start').disabled = busy || !loadedKey || loadedKey !== key() || !$('channel').value;
|
||||
$('stop').disabled = !running;
|
||||
$('channels').disabled = busy || loadingChannels;
|
||||
for (const id of ['key','channel','target']) $(id).disabled = busy || (id === 'channel' && !loadedKey);
|
||||
}
|
||||
async function post(path, body) {
|
||||
const response = await fetch('/api/' + path, {method:'POST',headers:{'Content-Type':'application/json','X-Session-Token':token},body:JSON.stringify(body)});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || '请求失败');
|
||||
return data;
|
||||
}
|
||||
$('key').addEventListener('input', () => {loadedKey=''; $('channel').replaceChildren(new Option('请重新获取频道','')); controls();});
|
||||
$('channels').addEventListener('click', async () => {
|
||||
loadingChannels=true; controls(); $('notice').textContent='正在获取频道…';
|
||||
const submittedKey = key();
|
||||
try {
|
||||
const data = await post('channels',{key:submittedKey});
|
||||
if (key() !== submittedKey) return;
|
||||
$('channel').replaceChildren(...data.channels.map(c => new Option(c,c)));
|
||||
loadedKey=submittedKey; $('notice').textContent=`已获取 ${data.channels.length} 个频道。`;
|
||||
$('debug-channel').value=$('channel').value;
|
||||
} catch(error) {$('notice').textContent=error.message;}
|
||||
finally {loadingChannels=false; controls();}
|
||||
});
|
||||
$('channel').addEventListener('change', () => {$('debug-channel').value=$('channel').value; controls();});
|
||||
$('download-form').addEventListener('submit', async event => {
|
||||
event.preventDefault(); starting=true; controls(); $('notice').textContent='';
|
||||
try {await post('start',{key:key(),channel:$('channel').value,target:Number($('target').value)}); await refresh();}
|
||||
catch(error) {$('notice').textContent=error.message;}
|
||||
finally {starting=false; controls();}
|
||||
});
|
||||
$('stop').addEventListener('click', async () => {
|
||||
$('stop').disabled=true;
|
||||
try {await post('stop',{}); $('notice').textContent='正在停止,等待当前请求或文章保存结束…';}
|
||||
catch(error) {$('notice').textContent=error.message;}
|
||||
});
|
||||
$('debug-form').addEventListener('submit', async event => {
|
||||
event.preventDefault(); $('debug-submit').disabled=true; $('debug-output').textContent='正在请求…';
|
||||
try {
|
||||
const data=await post('debug',{key:key(),endpoint:$('endpoint').value,channel:$('debug-channel').value,num:Number($('debug-num').value),start:Number($('debug-start').value)});
|
||||
const output=JSON.stringify(data,null,2);
|
||||
$('debug-output').textContent=output.slice(0,100000)+(output.length>100000?'\n…输出超过 100000 字符,已截断。':'');
|
||||
} catch(error) {$('debug-output').textContent=error.message;}
|
||||
finally {$('debug-submit').disabled=false;}
|
||||
});
|
||||
async function refresh() {
|
||||
const response=await fetch('/api/state');
|
||||
if (!response.ok) throw new Error('无法读取下载状态');
|
||||
const data=await response.json();
|
||||
running=data.status==='running'; controls();
|
||||
const percent=data.target ? Math.floor(data.saved/data.target*100) : 0;
|
||||
$('status').textContent=labels[data.status] || data.status;
|
||||
$('progress-text').textContent=`下载新闻 ${data.saved}/${data.target} 条`;
|
||||
$('percent').textContent=`已完成 ${percent}%`; $('progress').value=percent;
|
||||
for (const id of ['saved','skipped','failed','requests','message','output']) $(id).textContent=data[id];
|
||||
const latest=data.logs.length ? data.logs[data.logs.length-1].id : 0;
|
||||
if (latest!==lastLog) {
|
||||
const logs=$('logs'); const bottom=logs.scrollHeight-logs.scrollTop-logs.clientHeight<40;
|
||||
logs.replaceChildren(...data.logs.map(log => {const row=document.createElement('div'); const stamp=document.createElement('time'); stamp.textContent=log.time; row.append(stamp,document.createTextNode(log.message)); return row;}));
|
||||
if (bottom) logs.scrollTop=logs.scrollHeight;
|
||||
lastLog=latest;
|
||||
}
|
||||
}
|
||||
async function poll() {
|
||||
try {await refresh();}
|
||||
catch(error) {$('notice').textContent='与本地服务连接中断,请检查启动窗口。';}
|
||||
setTimeout(poll,700);
|
||||
}
|
||||
(async () => {
|
||||
try {const response=await fetch('/api/session'); token=(await response.json()).token; await poll();}
|
||||
catch(error) {$('notice').textContent='初始化失败,请刷新页面。';}
|
||||
})();
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>探数 · 新闻下载器</title><link rel="stylesheet" href="/style.css"><script src="/app.js" defer></script></head>
|
||||
<body><main>
|
||||
<header><div class="brand">TANSHU / NEWS ARCHIVE</div><h1>把新闻,保存到本地。</h1><p>选择频道,收集新闻。正文与图片一起保存,自动跳过已下载的内容。</p></header>
|
||||
<section class="card"><div class="section-heading"><h2>下载设置</h2><span class="badge">本地运行</span></div>
|
||||
<form id="download-form"><label for="key">API Key</label><div class="key-row"><input id="key" type="password" placeholder="填写探数 API Key" autocomplete="off" required maxlength="512"><button id="channels" type="button" class="secondary">获取新闻频道</button></div><p class="hint">密钥仅用于本次页面会话,不写入文件或浏览器存储。</p>
|
||||
<div class="fields"><div><label for="channel">新闻频道</label><select id="channel" required disabled><option value="">先填写密钥并获取频道</option></select></div><div><label for="target">下载数量</label><input id="target" type="number" min="1" max="100000" step="1" value="10" required></div></div>
|
||||
<p class="hint">接口每次最多 40 条,起始位置最多 400。遇到重复会继续向后查找;可用新闻不足时会说明原因。</p><div class="actions"><button id="start" disabled>开始下载</button><button id="stop" type="button" class="secondary" disabled>停止下载</button></div><p id="notice" role="alert"></p></form></section>
|
||||
<section class="card"><div class="section-heading"><h2>下载进度</h2><span id="status" class="badge">等待开始</span></div><div class="progress-heading"><strong id="progress-text">下载新闻 0/0 条</strong><span id="percent">已完成 0%</span></div><progress id="progress" max="100" value="0" aria-label="下载进度"></progress><div class="stats"><span>已保存 <b id="saved">0</b></span><span>重复跳过 <b id="skipped">0</b></span><span>保存失败 <b id="failed">0</b></span><span>接口请求 <b id="requests">0</b></span></div><p id="message">等待开始</p><p class="hint path">保存位置:<span id="output">加载中…</span></p></section>
|
||||
<section class="card"><div class="section-heading"><h2>运行输出</h2><span class="hint">保留最近 300 条</span></div><div id="logs" class="console" role="log" aria-label="运行输出"><div class="empty">运行情况将在这里显示。</div></div></section>
|
||||
<details class="card"><summary>调试终端 <span>手动请求接口</span></summary><p class="hint">使用上方 API Key。请求会消耗接口调用额度,但不会保存新闻。</p><form id="debug-form"><div class="fields"><div><label for="endpoint">请求接口</label><select id="endpoint"><option value="channel">新闻频道查询 /channel</option><option value="index">新闻列表查询 /index</option></select></div><div><label for="debug-channel">type · 频道</label><input id="debug-channel" value="头条"></div><div><label for="debug-num">num · 数量(1–40)</label><input id="debug-num" type="number" min="1" max="40" value="1" required></div><div><label for="debug-start">start · 起始位置(0–400)</label><input id="debug-start" type="number" min="0" max="400" value="0" required></div></div><button id="debug-submit" class="secondary">发送测试请求</button></form><pre id="debug-output" class="console">尚未发送请求。</pre></details>
|
||||
<footer>Markdown 正文 · 本地图片 · 历史去重</footer>
|
||||
</main></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
:root{font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif;color:#20312e;background:#f3f6f3;font-size:15px}*{box-sizing:border-box}body{margin:0}main{max-width:960px;margin:0 auto;padding:48px 24px 24px}header{margin-bottom:32px}.brand{font-size:12px;letter-spacing:2px;font-weight:700;color:#437b69}h1{font-size:36px;letter-spacing:-1px;margin:14px 0}header p{color:#687a74;line-height:1.8}h2{font-size:17px;margin:0}.card{padding:26px 28px;background:#fff;border:1px solid #dfe7e1;border-radius:16px;margin-bottom:20px;box-shadow:0 4px 16px #243c2904}.section-heading{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:24px}.badge{background:#edf4ef;color:#4c7564;padding:6px 10px;border-radius:20px;font-size:12px}.key-row{display:flex;gap:12px}.key-row input{flex:1;min-width:0}label{display:block;font-weight:600;margin-bottom:9px;font-size:13px}input,select,button{font:inherit;border-radius:8px}input,select{background:#fbfcfb;border:1px solid #d7e0d9;width:100%;padding:12px;color:inherit}input:focus,select:focus,button:focus-visible,summary:focus-visible{outline:3px solid #8dcab5;outline-offset:2px}button{cursor:pointer;border:1px solid #286850;background:#286850;color:#fff;padding:12px 22px;font-weight:600;white-space:nowrap}.secondary{background:#fff;color:#345e4f;border-color:#cedbd3}button:disabled{opacity:.45;cursor:not-allowed}.fields{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin:24px 0 16px}.hint{font-size:12px;color:#79867f;line-height:1.8;margin:10px 0}.actions{display:flex;gap:12px;margin-top:22px}#notice{color:#ac4c32;font-size:13px;white-space:pre-wrap}#notice:empty{display:none}.progress-heading{display:flex;justify-content:space-between;gap:16px;font-size:14px}#percent{color:#3f7b60}progress{display:block;width:100%;height:12px;border:0;border-radius:10px;overflow:hidden;margin:18px 0 20px;background:#edf2ef}progress::-webkit-progress-bar{background:#edf2ef}progress::-webkit-progress-value{background:#3c8b68;transition:width .25s}progress::-moz-progress-bar{background:#3c8b68}.stats{display:flex;gap:28px;flex-wrap:wrap;color:#7a8880;font-size:13px}.stats b{color:#304e40;margin-left:8px;font-size:17px}#message{font-size:13px;color:#52685b;line-height:1.8}.path{overflow-wrap:anywhere}.console{height:260px;overflow:auto;background:#15251f;color:#c5dacf;border-radius:10px;padding:18px;font:12px/1.9 ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.console time{color:#7eac94;margin-right:14px}.empty{color:#7e9889}summary{cursor:pointer;font-weight:600}summary span{font-size:12px;font-weight:400;color:#79867f;margin-left:12px}#debug-output{margin-bottom:0;height:300px}footer{text-align:center;color:#89958e;font-size:12px;margin:32px 0}@media(max-width:600px){main{padding:26px 14px}h1{font-size:28px}.card{padding:22px 18px}.key-row{flex-direction:column}.fields{grid-template-columns:1fr}.stats{gap:14px}.progress-heading{font-size:12px}}
|
||||
Reference in new issue
Block a user