circle.html
6.59 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
{% 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;">
<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>
</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 ──
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; }
document.getElementById('articleCount').textContent = j.data.length;
let html = '<table class="list-table"><thead><tr><th>标题</th><th>时间</th><th>状态</th><th>操作</th></tr></thead><tbody>';
for (const a of j.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;
} catch(e) { list.innerHTML = '<div class="empty-state">加载失败</div>'; }
}
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,'&').replace(/</g,'<').replace(/>/g,'>'); }
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 %}