articles.html
15.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
{% extends "base.html" %}
{% set request_path = "/articles" %}
{% block body %}
<style>
body{overflow-y:auto!important;height:auto!important;}
.fbar{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;align-items:center;}
.fbar input{flex:1;min-width:120px;padding:10px;border:1px solid #ddd;border-radius:8px;font-size:14px;}
.fbar select,.fbar button{padding:10px;border:1px solid #ddd;border-radius:8px;font-size:13px;background:white;cursor:pointer;}
.fbar button.active{background:#3498db;color:white;border-color:#3498db;}
.btn-cached{background:#e8f5e9!important;color:#888!important;border-color:#ccc!important;cursor:default!important;}
.pagination{display:flex;gap:6px;justify-content:center;margin:16px 0;flex-wrap:wrap;align-items:center;}
.pagination button{padding:8px 12px;border:1px solid #ddd;border-radius:6px;background:white;font-size:13px;cursor:pointer;}
.pagination button.active{background:#3498db;color:white;border-color:#3498db;}
.pagination button:disabled{opacity:.4;cursor:default;}
.pagination span{font-size:13px;color:#666;}
.pagination select{padding:8px;border:1px solid #ddd;border-radius:6px;font-size:13px;}
.date-input{padding:10px;border:1px solid #ddd;border-radius:8px;font-size:13px;width:130px;background:white;}
</style>
<div style="max-width:700px;margin:0 auto;padding:20px;">
<h2 style="margin-bottom:12px;">📑 文章列表</h2>
<div class="fbar">
<input type="text" id="searchInput" placeholder="搜索标题..." oninput="render()">
<input type="date" id="dateFrom" class="date-input" onchange="loadPage(1)">
<span style="color:#999;font-size:12px;">至</span>
<input type="date" id="dateTo" class="date-input" onchange="loadPage(1)">
<select id="groupFilter" onchange="render()"><option value="">全部群</option></select>
<button id="btnTime" class="active" onclick="setSort('time')">时间↓</button>
<button id="btnHot" onclick="setSort('hot')">热度↓</button>
<button id="btnSummarized" onclick="setStatusFilter('summarized')" style="padding:8px 12px;border:1px solid #27ae60;border-radius:8px;font-size:12px;background:white;color:#27ae60;cursor:pointer;">已摘要</button>
<button id="btnLearned" onclick="setStatusFilter('learned')" style="padding:8px 12px;border:1px solid #8e44ad;border-radius:8px;font-size:12px;background:white;color:#8e44ad;cursor:pointer;">已学习</button>
<select id="perPageSel" onchange="changePerPage()">
<option value="10">10条/页</option>
<option value="20" selected>20条/页</option>
<option value="50">50条/页</option>
</select>
<button id="btnRefresh" onclick="manualRefresh()" style="padding:10px 14px;border:1px solid #e67e22;border-radius:8px;font-size:13px;background:white;color:#e67e22;cursor:pointer;white-space:nowrap;margin-left:auto;">🔄 手动刷新</button>
</div>
<div style="font-size:12px;color:#999;margin-bottom:12px;" id="infoBar">加载中...</div>
<div id="pagerTop"></div>
<div id="articleList"><div class="loading"><div class="spinner"></div></div></div>
<div id="pagerBottom"></div>
</div>
<script>
var allArticles=[], sortBy='time', sortAsc=false;
var currentPage=1, totalPages=1, perPage=20, totalCount=0;
var cachedSummaries={}, cachedLearns={};
var statusFilter='';
var allGroups=[];
async function checkCache(){
if(!allArticles.length)return;
var urls=[];
for(var i=0;i<allArticles.length;i++){
urls.push({url:allArticles[i].url,title:allArticles[i].title});
}
try{
var r1=await fetch('/api/articles/check-summaries',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({urls:urls,type:'summary'})});
var j1=await r1.json();
if(j1.ok&&j1.data.results){cachedSummaries=j1.data.results;}
var r2=await fetch('/api/articles/check-summaries',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({urls:urls,type:'learn'})});
var j2=await r2.json();
if(j2.ok&&j2.data.results){cachedLearns=j2.data.results;}
}catch(e){}
render();
}
async function loadPage(page){
currentPage=page;
document.getElementById('articleList').innerHTML='<div class="loading"><div class="spinner"></div></div>';
try{
var df=document.getElementById('dateFrom').value;
var dt=document.getElementById('dateTo').value;
var apiUrl='/api/articles?page='+page+'&per_page='+perPage;
if(df)apiUrl+='&date_from='+df;
if(dt)apiUrl+='&date_to='+dt;
var r=await fetch(apiUrl);
var j=await r.json();
if(!j.ok||!j.data.length){document.getElementById('articleList').innerHTML='<div class="empty-state">暂无文章</div>';return;}
allArticles=j.data;
totalCount=j.total;
totalPages=Math.ceil(totalCount/perPage);
if(!allGroups.length){
try{
var rAll=await fetch('/api/articles?page=1&per_page='+totalCount+(df?'&date_from='+df:'')+(dt?'&date_to='+dt:''));
var jAll=await rAll.json();
if(jAll.ok){
var ag=new Set();
for(var i=0;i<jAll.data.length;i++){var gs=jAll.data[i].groups||[];for(var jj=0;jj<gs.length;jj++)ag.add(gs[jj]);}
allGroups=Array.from(ag).sort();
var sel=document.getElementById('groupFilter');
sel.innerHTML='<option value="">全部群</option>';
for(var k=0;k<allGroups.length;k++)sel.innerHTML+='<option value="'+esc(allGroups[k])+'">'+esc(allGroups[k])+'</option>';
}
}catch(e){}
}
render();
checkCache();
renderPager();
}catch(e){document.getElementById('articleList').innerHTML='<div class="empty-state">加载失败: '+e.message+'</div>';}
}
function renderPager(){
var html='';
html+='<span>共 '+totalCount+' 篇,'+totalPages+' 页</span>';
html+='<button '+(currentPage<=1?'disabled':'')+' onclick="loadPage('+(currentPage-1)+')">上一页</button>';
var start=Math.max(1,currentPage-2),end=Math.min(totalPages,currentPage+2);
if(start>1){html+='<button onclick="loadPage(1)">1</button>';if(start>2)html+='<span>…</span>';}
for(var i=start;i<=end;i++){
html+='<button '+(i===currentPage?'class="active"':'')+' onclick="loadPage('+i+')">'+i+'</button>';
}
if(end<totalPages){if(end<totalPages-1)html+='<span>…</span>';html+='<button onclick="loadPage('+totalPages+')">'+totalPages+'</button>';}
html+='<button '+(currentPage>=totalPages?'disabled':'')+' onclick="loadPage('+(currentPage+1)+')">下一页</button>';
document.getElementById('pagerTop').innerHTML='<div class="pagination">'+html+'</div>';
document.getElementById('pagerBottom').innerHTML='<div class="pagination">'+html+'</div>';
}
function changePerPage(){
perPage=parseInt(document.getElementById('perPageSel').value);
currentPage=1;
loadPage(1);
}
function getDedupKey(url){
if(!url||url.indexOf('mp.weixin.qq.com')<0)return url;
var m=url.match(/__biz=([^&]+)/),mid=url.match(/mid=(\d+)/),idx=url.match(/idx=(\d+)/),sn=url.match(/sn=([a-f0-9]+)/);
return m&&mid&&idx&&sn?m[1]+'|'+mid[1]+'|'+idx[1]+'|'+sn[1]:url;
}
function isCached(url,type){
var dk=getDedupKey(url);
return type==='summary'?!!cachedSummaries[dk]:!!cachedLearns[dk];
}
function getCachedInfo(url,type){
var dk=getDedupKey(url);
if(type==='summary')return cachedSummaries[dk]||null;
return cachedLearns[dk]||null;
}
function setStatusFilter(f){statusFilter=statusFilter===f?'':f;document.getElementById('btnSummarized').style.background=statusFilter==='summarized'?'#27ae60':'white';document.getElementById('btnSummarized').style.color=statusFilter==='summarized'?'white':'#27ae60';document.getElementById('btnLearned').style.background=statusFilter==='learned'?'#8e44ad':'white';document.getElementById('btnLearned').style.color=statusFilter==='learned'?'white':'#8e44ad';render();}
function setSort(s){
if(sortBy===s)sortAsc=!sortAsc;else{sortBy=s;sortAsc=false;}
document.getElementById('btnTime').className=sortBy==='time'?'active':'';
document.getElementById('btnHot').className=sortBy==='hot'?'active':'';
document.getElementById('btnTime').textContent=sortBy==='time'?(sortAsc?'时间↑':'时间↓'):'时间↓';
document.getElementById('btnHot').textContent=sortBy==='hot'?(sortAsc?'热度↑':'热度↓'):'热度↓';
render();
}
function render(){
var q=(document.getElementById('searchInput').value||'').trim().toLowerCase(),
gf=document.getElementById('groupFilter').value,
filtered=[];
for(var i=0;i<allArticles.length;i++){
var a=allArticles[i];
if(q&&(a.title||'').toLowerCase().indexOf(q)<0)continue;
if(gf&&(a.groups||[]).indexOf(gf)<0)continue;
if(statusFilter==='summarized'&&!isCached(a.url,'summary'))continue;
if(statusFilter==='learned'&&!isCached(a.url,'learn'))continue;
filtered.push(a);
}
filtered.sort(function(a,b){
if(sortBy==='time')return sortAsc?(a.createTime||0)-(b.createTime||0):(b.createTime||0)-(a.createTime||0);
else return sortAsc?a.shareCount-b.shareCount:b.shareCount-a.shareCount;
});
var cs=0,cl=0;
for(var i=0;i<filtered.length;i++){if(isCached(filtered[i].url,'summary'))cs++;if(isCached(filtered[i].url,'learn'))cl++;}
var statusLabel=statusFilter==='summarized'?' · 已摘要':statusFilter==='learned'?' · 已学习':'';document.getElementById('infoBar').textContent='第 '+currentPage+' 页 · '+filtered.length+' 篇(已摘要 '+cs+' / 已学习 '+cl+')'+statusLabel+(q||gf?'(筛选自 '+totalCount+' 篇':'');
var html='';
for(var i=0;i<filtered.length;i++){
var a=filtered[i],
t=new Date(a.createTime||0),
ts=t.toLocaleDateString()+' '+t.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
html+='<div class="article-card" style="display:block;">';
html+='<a href="'+esc(a.url)+'" target="_blank" style="text-decoration:none;color:inherit;display:flex;gap:10px;">';
html+='<div class="card-thumb" style="display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">📄</div>';
html+='<div class="card-body" style="flex:1;min-width:0;">';
html+='<div class="card-title">'+esc(a.title||'')+'</div>';
if(a.description)html+='<div class="card-desc">'+esc(a.description)+'</div>';
html+='<div class="card-source" style="display:flex;justify-content:space-between;"><span>📨 '+ts+'</span><span style="color:#e67e22;font-weight:600;">🔥 '+a.shareCount+'群</span></div>';
if(a.groups&&a.groups.length)html+='<div style="font-size:10px;color:#bbb;margin-top:3px;">'+a.groups.join(' · ')+'</div>';
html+='</div></a>';
html+='<div style="display:flex;gap:8px;margin-top:10px;">';
var ci=getCachedInfo(a.url,'summary');
if(ci){
html+='<button onclick="event.preventDefault();summarize(event,\''+escAttr(a.title)+'\',\''+escAttr(a.url)+'\')" style="flex:1;padding:10px;border:1px solid #27ae60;border-radius:8px;background:white;color:#27ae60;font-size:13px;cursor:pointer;">🔄 重新摘要</button>';
html+='<a href="/report/'+ci.id+'" target="_blank" style="padding:10px 14px;border:1px solid #27ae60;border-radius:8px;background:#e8f5e9;color:#27ae60;font-size:13px;text-decoration:none;white-space:nowrap;">📄 查看</a>';
}else{
html+='<button onclick="event.preventDefault();summarize(event,\''+escAttr(a.title)+'\',\''+escAttr(a.url)+'\')" style="flex:1;padding:10px;border:1px solid #3498db;border-radius:8px;background:white;color:#3498db;font-size:13px;cursor:pointer;">📝 摘要</button>';
}
var li=getCachedInfo(a.url,'learn');
if(li){
html+='<button onclick="event.preventDefault();learnArticle(event,\''+escAttr(a.title)+'\',\''+escAttr(a.url)+'\')" style="flex:1;padding:10px;border:1px solid #8e44ad;border-radius:8px;background:white;color:#8e44ad;font-size:13px;cursor:pointer;">🔄 重新学习</button>';
html+='<a href="/report/'+li.id+'" target="_blank" style="padding:10px 14px;border:1px solid #8e44ad;border-radius:8px;background:#f3e5f5;color:#8e44ad;font-size:13px;text-decoration:none;white-space:nowrap;">📄 查看</a>';
}else{
html+='<button onclick="event.preventDefault();learnArticle(event,\''+escAttr(a.title)+'\',\''+escAttr(a.url)+'\')" style="flex:1;padding:10px;border:1px solid #9b59b6;border-radius:8px;background:white;color:#9b59b6;font-size:13px;cursor:pointer;">🔧 学习</button>';
}
html+='</div></div>';
}
document.getElementById('articleList').innerHTML=html||'<div class="empty-state">无匹配文章</div>';
}
function esc(s){return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
function escAttr(s){return String(s||'').replace(/'/g,"\\'").replace(/\\/g,'\\\\');}
async function summarize(evt,title,url){evt.stopPropagation();evt.preventDefault();if(!confirm('为这篇文章生成摘要?'))return;
try{var r=await fetch('/api/articles/summarize',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({title:title,url:url})});var j=await r.json();
if(!j.ok){alert('失败: '+(j.error||''));return;}
if(j.data.status==='cached'){if(!confirm('已有摘要('+j.data.created_at+'),要重新生成吗?'))return;}
alert('摘要任务已提交!刷新页面可查看。');
var dk=getDedupKey(url);cachedSummaries[dk]=true;render();
}catch(e){alert('错误: '+e.message);}}
async function learnArticle(evt,title,url){evt.stopPropagation();evt.preventDefault();if(!confirm('学习这篇文章并构建 skill?'))return;
try{var r=await fetch('/api/articles/learn',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({title:title,url:url})});var j=await r.json();
if(!j.ok){alert('失败: '+(j.error||''));return;}
if(j.data.status==='cached'){if(!confirm('已有学习结果('+j.data.created_at+'),要重新学习吗?'))return;}
alert('学习任务已提交!刷新页面可查看。');
var dk=getDedupKey(url);cachedLearns[dk]=true;render();
}catch(e){alert('错误: '+e.message);}}
async function manualRefresh(){
var btn=document.getElementById('btnRefresh');
if(btn.disabled)return;
btn.disabled=true;
btn.textContent='⏳ 提取中...';
btn.style.opacity='0.6';
try{
var r=await fetch('/api/articles/refresh',{method:'POST'});
var j=await r.json();
if(!j.ok){alert('启动失败: '+(j.error||''));btn.disabled=false;btn.textContent='🔄 手动刷新';btn.style.opacity='1';return;}
var tid=j.data.task_id;
var start=Date.now(),done=false;
while(Date.now()-start<120000){
await new Promise(function(res){setTimeout(res,3000);});
var pr=await fetch('/api/job/'+tid);
var pj=await pr.json();
if(!pj.ok)continue;
var st=pj.data.status;
if(st==='completed'){done=true;break;}
if(st==='failed'){break;}
btn.textContent='⏳ 提取中... ('+Math.round((Date.now()-start)/1000)+'s)';
}
if(done){
try{
var lr=await fetch('/api/job/'+tid+'/log');
var lj=await lr.json();
if(lj.ok&&lj.data.content){
var c=lj.data.content;
var m=c.match(/(\d+)\s+new articles/);
if(m)alert('✅ 提取完成!新增 '+m[1]+' 篇文章。');
else alert('✅ 提取完成!');
}else alert('✅ 提取完成!');
}catch(e){alert('✅ 提取完成!');}
}else{
alert('⚠️ 提取未完成或超时,请查看 /jobs 页面了解详情。');
}
}catch(e){alert('错误: '+e.message);}
btn.disabled=false;
btn.textContent='🔄 手动刷新';
btn.style.opacity='1';
loadPage(currentPage);
}
loadPage(1);
</script>
{% endblock %}