articles.html 8.62 KB
{% 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
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 %}