articles.html
8.62 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
{% 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;}
</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()">
<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>
</div>
<div style="font-size:12px;color:#999;margin-bottom:12px;" id="infoBar">加载中...</div>
<div id="articleList"><div class="loading"><div class="spinner"></div></div></div>
</div>
<script>
var allArticles=[],sortBy='time',sortAsc=false;
var cachedSummaries={}, cachedLearns={};
async function checkCache(){
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 load(){
var list=document.getElementById('articleList');
try{var r=await fetch('/api/articles');var j=await r.json();
if(!j.ok||!j.data.length){list.innerHTML='<div class="empty-state">暂无文章</div>';return;}
allArticles=j.data;var ag=new Set();
for(var i=0;i<allArticles.length;i++){var gs=allArticles[i].groups||[];for(var j=0;j<gs.length;j++)ag.add(gs[j]);}
var sel=document.getElementById('groupFilter'),names=Array.from(ag).sort();
for(var k=0;k<names.length;k++)sel.innerHTML+='<option value="'+esc(names[k])+'">'+esc(names[k])+'</option>';
checkCache();
}catch(e){list.innerHTML='<div class="empty-state">加载失败</div>';}
}
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 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;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++;}
document.getElementById('infoBar').textContent='共 '+filtered.length+' 篇(已有摘要 '+cs+' / 已学习 '+cl+')'+(q||gf?'(筛选自 '+allArticles.length+' 篇)':'');
var html='';
for(var i=0;i<filtered.length;i++){var a=filtered[i],t=new Date((a.createTime||0)*1000),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;">';
if(isCached(a.url,'summary')){
html+='<button class="btn-cached" style="flex:1;padding:10px;border-radius:8px;font-size:13px;">✅ 已有摘要</button>';
}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>';
}
if(isCached(a.url,'learn')){
html+='<button class="btn-cached" style="flex:1;padding:10px;border-radius:8px;font-size:13px;">✅ 已学习</button>';
}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 genArticleImg(evt, title, url) {
evt.stopPropagation(); evt.preventDefault();
if (!confirm('为这篇文章生成复古铜版画图片?')) return;
try {
var r = await fetch('/api/articles/generate-image', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({title:title,url:url})});
var j = await r.json();
if (j.ok) alert('图片生成任务已提交!稍后查看日报图片列表。');
else alert('失败: '+(j.error||''));
} catch(e) { alert('错误: '+e.message); }
}
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'){alert('已有摘要('+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'){alert('已有学习结果('+j.data.created_at+')无需重新生成。');return;}
alert('学习任务已提交!刷新页面可查看。');
var dk=getDedupKey(url);cachedLearns[dk]=true;render();
}catch(e){alert('错误: '+e.message);}}
load();
</script>
{% endblock %}