52 lines
7.0 KiB
JavaScript
52 lines
7.0 KiB
JavaScript
'use strict';
|
||
const $=id=>document.getElementById(id);
|
||
let token='',state=null,loadedKey='',refreshing=false;
|
||
const labels={queued:'待处理',preparing:'准备中',importing:'上传与导入',formatting:'排版中',awaiting_user_review:'等待人工审核',user_submitting:'已观察到人工提交',submit_unknown:'发布结果待核对',accepted_pending_moderation:'已受理 · 审核中',published:'已确认公开',needs_attention:'需要处理',cancelled:'已移出',skipped:'已跳过'};
|
||
function notice(message){$('notice').hidden=!message;$('notice').textContent=message;}
|
||
async function api(route,body={}) {
|
||
const response=await fetch('/api/'+route,{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;
|
||
}
|
||
function action(id,fn){$(id).addEventListener('click',async()=>{const button=$(id);button.disabled=true;try{await fn();await refresh();}catch(error){notice(error.message);}finally{button.disabled=false;}});}
|
||
function text(tag,value,className){const e=document.createElement(tag);e.textContent=value;if(className)e.className=className;return e;}
|
||
function render(data){
|
||
state=data;const {jobs,run,download}=data;
|
||
$('total').textContent=jobs.length;$('review').textContent=jobs.filter(j=>j.state==='awaiting_user_review').length;
|
||
$('accepted').textContent=jobs.filter(j=>['published','accepted_pending_moderation'].includes(j.state)).length;
|
||
$('batch').textContent=run.batch?`${run.batch.count} / ${run.batch.target}`:'—';$('account').textContent=run.batch?run.batch.account.name:'尚未绑定账号';
|
||
$('run-state').textContent=run.running?'流程运行中':'流程已暂停';$('run-reason').textContent=run.reason;
|
||
$('run-light').classList.toggle('gray',!run.running);$('empty').hidden=jobs.length>0;
|
||
$('jobs').replaceChildren(...jobs.map(job=>{
|
||
const tr=document.createElement('tr'),title=document.createElement('td');title.append(text('div',job.title),text('small',`${job.image_count} 张配图 · ${job.source}`));if(job.original_title!==job.title)title.append(text('small','已缩短标题,原题:'+job.original_title));
|
||
const status=document.createElement('td');status.append(text('span',labels[job.state]||job.state,'badge'));if(job.error)status.append(text('small',job.error));
|
||
const account=text('td',job.account?`${job.account.name} / ${job.fid}`:'待领取');const result=document.createElement('td');
|
||
if(job.post_url){const link=text('a','查看帖子');link.href=job.post_url;link.target='_blank';link.rel='noopener';result.append(link);}
|
||
if(!['published','accepted_pending_moderation'].includes(job.state)){
|
||
const retry=text('button','人工核对 / 处理');retry.className='secondary';retry.onclick=async()=>{
|
||
if(state.run.running){notice('请先暂停流程,再处理任务');return;}
|
||
if(!confirm('只有确认网站未受理这篇文章,且已清理编辑器/草稿后才能继续。\n已受理、审核中或结果不明的文章请取消。\n你是否已经确认未受理?'))return;
|
||
const again=confirm('重新加入队列?\n确定:重新排队。取消:移出队列。');
|
||
try{await api('resolve',{id:job.id,confirmed_not_accepted:true,retry:again});await refresh();}catch(error){notice(error.message);}
|
||
};result.append(retry);
|
||
}
|
||
tr.append(title,status,account,result);return tr;
|
||
}));
|
||
const dlLabels={idle:'等待开始',running:'下载中',completed:'下载完成',partial:'未达到目标',cancelled:'已停止',error:'下载出错'};
|
||
$('download-state').textContent=dlLabels[download.status]||download.status;$('download-progress').textContent=`已保存 ${download.saved}/${download.target} 篇 · 跳过 ${download.skipped} · 失败 ${download.failed}`;
|
||
$('progress').value=download.target?Math.floor(download.saved/download.target*100):0;
|
||
$('download').disabled=download.status==='running';$('logs').textContent=download.logs.map(x=>`${x.time} ${x.message}`).join('\n')||'暂无日志';
|
||
$('browser-state').textContent=data.clients.length?`${data.clients.length} 个标签在线`:'尚未连接';
|
||
$('clients').textContent=data.clients.map(c=>`${c.account?.name||'未识别账号'} · 换号插件${c.extension?.ready?'已就绪':'未连接或需更新'}`).join('\n')||'没有在线执行标签';
|
||
}
|
||
async function refresh(){if(refreshing||!token)return;refreshing=true;try{render(await api('state'));}catch(error){notice(error.message);$('run-state').textContent='本地服务连接中断';}finally{refreshing=false;}}
|
||
action('pair',async()=>{const value=await api('pair');$('pair-code').hidden=false;$('pair-code').type='text';$('pair-code').value=value.token;$('pair-code').select();});
|
||
action('channels',async()=>{const value=await api('channels',{key:$('key').value.trim()});loadedKey=$('key').value.trim();$('channel').replaceChildren(...value.channels.map(c=>new Option(c,c)));});
|
||
$('key').oninput=()=>{loadedKey='';$('channel').replaceChildren(new Option('请重新获取频道',''));};
|
||
$('download-form').onsubmit=async event=>{event.preventDefault();try{if(!loadedKey||loadedKey!==$('key').value.trim())throw new Error('请先获取频道');await api('download',{key:loadedKey,channel:$('channel').value,target:Number($('target').value)});notice('下载已开始,完整保存的文章会自动入队');await refresh();}catch(error){notice(error.message);}};
|
||
action('download-stop',()=>api('download-stop'));
|
||
action('scan',async()=>{notice('正在检查文章目录…');const value=await api('scan',{root:$('root').value.trim()});$('scan-result').textContent=value.results.map(x=>`${x.directory}:${x.status}`).join('\n')||'目录中未找到文章';notice('目录检查完成');});
|
||
$('config-form').onsubmit=async event=>{event.preventDefault();try{await api('configure',{fid:Number($('fid').value),minimum:Number($('minimum').value),maximum:Number($('maximum').value),interval:Number($('interval').value),history_checked:$('history-checked').checked});notice('处理规则已保存');await refresh();}catch(error){notice(error.message);}};
|
||
for(const name of ['start','pause','stop'])action(name,async()=>{await api(name);notice(name==='stop'?'流程已终止;浏览器将在下一次连接检查时停止后续操作。已发出的请求可能仍已完成。':'');});
|
||
$('history-file').onchange=async()=>{try{const file=$('history-file').files[0];if(!file)return;if(file.size>2*1024*1024)throw new Error('记录文件过大');const value=JSON.parse(await file.text());await api('history',{records:value.records});notice('历史记录已导入;仅使用标记为 block_repost 的记录');await refresh();}catch(error){notice(error.message);}};
|
||
(async()=>{try{const response=await fetch('/api/session');const session=await response.json();if(!response.ok)throw new Error(session.error);token=session.token;$('port').value=location.port;await refresh();if(state?.run.config?.fid){for(const id of ['fid','minimum','maximum','interval'])$(id).value=state.run.config[id];$('history-checked').checked=state.run.config.history_checked;}}catch(error){notice(error.message);}setInterval(refresh,2500);})();
|