circle.html 7.76 KB
{% extends "base.html" %}
{% set request_path = "/circle" %}

{% block body %}
<div id="toast" class="toast"></div>

<!-- Stats -->
<div class="stats-bar" id="statsBar">
  <div class="stat-item"><div class="num" id="statArticles">-</div><div class="label">文章总数</div></div>
  <div class="stat-item"><div class="num" id="statPending">-</div><div class="label">待处理</div></div>
  <div class="stat-item"><div class="num" id="statImages">-</div><div class="label">图片分析</div></div>
  <div class="stat-item"><div class="num" id="statSummaries">-</div><div class="label">群聊总结</div></div>
</div>

<div class="circle-grid">
  <!-- 文章队列 -->
  <div class="circle-card" style="grid-row: span 2;">
    <h3>📑 文章队列 <span class="count" id="articleCount">-</span></h3>
    <div style="margin-bottom:10px;display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
      <select id="articleFilter" onchange="loadArticles()" style="font-size:12px;padding:4px 8px;border:1px solid #ddd;border-radius:4px;">
        <option value="pending">待处理</option>
        <option value="read">已读</option>
        <option value="summarized">已总结</option>
        <option value="archived">已归档</option>
      </select>
      <input type="text" id="searchInput" placeholder="搜索标题..." oninput="renderArticles()" style="font-size:12px;padding:4px 8px;border:1px solid #ddd;border-radius:4px;min-width:120px;">
      <select id="sortBy" onchange="renderArticles()" style="font-size:12px;padding:4px 8px;border:1px solid #ddd;border-radius:4px;">
        <option value="time">按时间↓</option>
        <option value="title">按标题</option>
      </select>
    </div>
    <div id="articleList" style="max-height:400px;overflow-y:auto;">
      <div class="loading"><div class="spinner"></div></div>
    </div>
  </div>

  <!-- 图片分析 -->
  <div class="circle-card">
    <h3>🖼 图片分析</h3>
    <div id="imageList" style="max-height:300px;overflow-y:auto;">
      <div class="loading"><div class="spinner"></div></div>
    </div>
  </div>

  <!-- 群聊总结 -->
  <div class="circle-card">
    <h3>📊 群聊日总结</h3>
    <div id="summaryList" style="max-height:300px;overflow-y:auto;">
      <div class="loading"><div class="spinner"></div></div>
    </div>
  </div>
</div>

<script>
// ── Stats ──
async function loadStats() {
  try {
    const r = await fetch('/api/circle/stats');
    const j = await r.json();
    if (j.ok) {
      document.getElementById('statArticles').textContent = j.data.articles_total;
      document.getElementById('statPending').textContent = j.data.articles_pending;
      document.getElementById('statImages').textContent = j.data.images_total;
      document.getElementById('statSummaries').textContent = j.data.summaries_total;
    }
  } catch(e) {}
}

// ── Articles ──
var allArticles = [];

async function loadArticles() {
  const status = document.getElementById('articleFilter').value;
  const list = document.getElementById('articleList');
  list.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
  try {
    const r = await fetch('/api/circle/articles?status=' + status);
    const j = await r.json();
    if (!j.ok) { list.innerHTML = '<div class="empty-state">暂无数据</div>'; return; }
    if (!j.data.length) { list.innerHTML = '<div class="empty-state">暂无文章</div>'; return; }
    allArticles = j.data;
    renderArticles();
  } catch(e) { list.innerHTML = '<div class="empty-state">加载失败</div>'; }
}

async function renderArticles() {
  const list = document.getElementById('articleList');
  const q = (document.getElementById('searchInput').value || '').trim().toLowerCase();
  const sb = document.getElementById('sortBy').value;
  let data = allArticles.slice();
  if (q) data = data.filter(function(a){ return (a.title || '').toLowerCase().indexOf(q) >= 0; });
  if (sb === 'title') data.sort(function(a,b){ return (a.title||'').localeCompare(b.title||''); });
  else data.sort(function(a,b){ return (b.extracted_at||'').localeCompare(a.extracted_at||''); });
  document.getElementById('articleCount').textContent = data.length;
  if (!data.length) { list.innerHTML = '<div class="empty-state">无匹配</div>'; return; }
  let html = '<table class="list-table"><thead><tr><th>标题</th><th>时间</th><th>状态</th><th>操作</th></tr></thead><tbody>';
  for (const a of data) {
    const tagCls = a.status === 'pending' ? 'pending' : 'read';
    html += `<tr>
      <td><a href="${escHtml(a.url)}" target="_blank" style="font-size:13px;">${escHtml(a.title || '无标题')}</a></td>
      <td style="font-size:11px;color:#999;">${(a.extracted_at||'').slice(0,10)}</td>
      <td><span class="tag ${tagCls}">${a.status}</span></td>
      <td>
        ${a.status === 'pending' ? `<button onclick="markRead(${a.id})" style="font-size:11px;padding:2px 8px;border:1px solid #ddd;border-radius:4px;background:white;cursor:pointer;">✓ 已读</button>` : ''}
      </td>
    </tr>`;
  }
  html += '</tbody></table>';
  list.innerHTML = html;
}

async function markRead(id) {
  try {
    await fetch(`/api/circle/articles/${id}/status`, {
      method: 'POST', headers: {'Content-Type':'application/json'},
      body: JSON.stringify({status:'read'})
    });
    loadArticles(); loadStats();
    toast('已标记为已读');
  } catch(e) { toast('操作失败'); }
}

// ── Images ──
async function loadImages() {
  const list = document.getElementById('imageList');
  list.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
  try {
    const r = await fetch('/api/circle/images');
    const j = await r.json();
    if (!j.ok || !j.data.length) { list.innerHTML = '<div class="empty-state">暂无图片分析</div>'; return; }
    let html = '<table class="list-table"><thead><tr><th>图片</th><th>OCR</th><th>时间</th></tr></thead><tbody>';
    for (const img of j.data) {
      html += `<tr>
        <td>${img.image_path ? `<img src="/media/image?path=${encodeURIComponent(img.image_path)}" style="max-width:60px;max-height:60px;border-radius:4px;">` : '🖼'}</td>
        <td style="font-size:12px;max-width:200px;">${escHtml((img.ocr_text||'') + (img.description||''))}</td>
        <td style="font-size:11px;color:#999;">${(img.analyzed_at||'').slice(0,16)}</td>
      </tr>`;
    }
    html += '</tbody></table>';
    list.innerHTML = html;
  } catch(e) { list.innerHTML = '<div class="empty-state">加载失败</div>'; }
}

// ── Summaries ──
async function loadSummaries() {
  const list = document.getElementById('summaryList');
  list.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
  try {
    const r = await fetch('/api/circle/summaries');
    const j = await r.json();
    if (!j.ok || !j.data.length) { list.innerHTML = '<div class="empty-state">暂无总结</div>'; return; }
    let html = '<table class="list-table"><thead><tr><th>日期</th><th>群</th><th>消息数</th><th>总结</th></tr></thead><tbody>';
    for (const s of j.data) {
      html += `<tr>
        <td style="font-size:12px;">${s.date}</td>
        <td style="font-size:12px;">${escHtml(s.chatroom_id)}</td>
        <td style="font-size:12px;">${s.msg_count}</td>
        <td style="font-size:12px;max-width:250px;">${escHtml((s.summary||'').slice(0,100))}</td>
      </tr>`;
    }
    html += '</tbody></table>';
    list.innerHTML = html;
  } catch(e) { list.innerHTML = '<div class="empty-state">加载失败</div>'; }
}

// ── Utils ──
function escHtml(s) { return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function toast(msg) {
  const t = document.getElementById('toast');
  t.textContent = msg; t.classList.add('show');
  setTimeout(() => t.classList.remove('show'), 2000);
}

// ── Init ──
loadStats();
loadArticles();
loadImages();
loadSummaries();
</script>
{% endblock %}