Files
news-tracker/templates/logs.html
T

221 lines
10 KiB
HTML

{% extends "base.html" %}
{% set active = 'logs' %}
{% block title %}通知日志 - 新闻智能跟踪{% endblock %}
{% block content %}
<div class="page-head">
<h1>✉️ 通知日志 <span class="muted" id="total_label"></span></h1>
<div class="actions">
<button class="btn" onclick="testMail()">📮 发送测试邮件</button>
<button class="btn" onclick="testErrorMail()">⚠️ 测试错误通知</button>
</div>
</div>
<div class="filters">
<select id="f_type" onchange="loadPage(1)">
<option value="">全部类型</option>
<option value="realtime">🔥 实时重要资讯</option>
<option value="summary">📰 新闻汇总</option>
<option value="custom_summary">🎯 定制监控汇总</option>
<option value="error">⚠️ 系统错误</option>
</select>
<select id="f_status" onchange="loadPage(1)">
<option value="">全部状态</option>
<option value="ok">✅ 成功</option>
<option value="error">❌ 失败</option>
</select>
<input type="text" id="f_q" placeholder="搜索主题 / 详情..." onkeydown="if(event.key==='Enter')loadPage(1)">
<button class="btn" onclick="loadPage(1)">筛选</button>
<button class="btn" onclick="resetFilter()">重置</button>
<span class="muted small" style="margin-left:auto;">勾选行后可批量删除</span>
</div>
<div class="card">
<div class="form-row" style="justify-content:flex-start;margin-bottom:8px;">
<label style="display:flex;align-items:center;gap:4px;font-size:13px;">
<input type="checkbox" id="check_all" onchange="toggleAll(this)"> 全选本页
</label>
<button class="btn mini danger" onclick="delSelected()">🗑 删除选中 (<span id="sel_count">0</span>)</button>
<button class="btn mini" onclick="delFiltered()">🗑 删除当前筛选结果全部</button>
</div>
<table>
<thead><tr><th style="width:34px;"></th><th>ID</th><th>类型</th><th>主题(点击查看内容)</th><th>条数</th><th>状态</th><th>发送时间</th><th>操作</th></tr></thead>
<tbody id="log_tbody">
<tr><td colspan="8" class="empty">⏳ 加载中...</td></tr>
</tbody>
</table>
</div>
<div class="pager" id="pager"></div>
<!-- 查看通知内容 模态框 -->
<div class="modal-mask" id="viewModal" style="display:none;">
<div class="modal" style="width:720px;">
<div class="modal-head">
<h3 id="viewTitle">通知内容</h3>
<span class="modal-x" onclick="document.getElementById('viewModal').style.display='none'"></span>
</div>
<div class="modal-body" id="viewBody" style="padding:12px 16px;"></div>
<div class="modal-foot">
<button class="btn" onclick="document.getElementById('viewModal').style.display='none'">关闭</button>
</div>
</div>
</div>
{% endblock %}
{% block script %}
<script>
let CUR = {page:1, page_size:20, total:0, pages:1};
const TYPE_TAG = {
realtime: '<span class="tag hot">🔥 实时</span>',
summary: '<span class="tag ok">📰 新闻汇总</span>',
custom_summary: '<span class="tag">🎯 定制汇总</span>',
error: '<span class="tag err">⚠️ 系统错误</span>'
};
const esc = s => (s||'').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
async function loadPage(page){
const qs = {
page: Math.max(1, page),
page_size: document.getElementById('f_page_size') ? document.getElementById('f_page_size').value : CUR.page_size,
type: document.getElementById('f_type').value,
status: document.getElementById('f_status').value,
q: document.getElementById('f_q').value
};
const r = await API.get('/api/logs', qs);
if (!r.ok){ document.getElementById('log_tbody').innerHTML = '<tr><td colspan="8" class="empty">加载失败</td></tr>'; return; }
CUR = r;
renderRows(r.logs);
renderPager(r);
}
function renderRows(logs){
const tb = document.getElementById('log_tbody');
if (!logs.length){
tb.innerHTML = '<tr><td colspan="8" class="empty">暂无匹配的通知记录</td></tr>';
document.getElementById('total_label').textContent = '';
return;
}
document.getElementById('total_label').textContent = '共 ' + CUR.total + ' 条';
tb.innerHTML = logs.map(l => `
<tr data-lid="${l.id}">
<td><input type="checkbox" class="row_check" onchange="updSel()"></td>
<td>${l.id}</td>
<td>${TYPE_TAG[l.type] || '<span class="tag">'+esc(l.type)+'</span>'}</td>
<td><a href="javascript:void(0)" onclick="viewLog(${l.id})" style="color:#2563eb;">${esc(l.subject)}</a>
${l.detail ? '<div class="muted small">' + esc(l.detail) + '</div>' : ''}
</td>
<td>${l.count}</td>
<td>${l.status==='ok' ? '<span class="tag ok">成功</span>' : '<span class="tag err">失败</span>'}</td>
<td class="small">${esc(l.sent_at)}</td>
<td style="white-space:nowrap;">
<button class="btn mini" onclick="viewLog(${l.id})">👁 查看</button>
<button class="btn mini danger" onclick="delOne(${l.id})">删除</button>
</td>
</tr>`).join('');
updSel();
}
function renderPager(r){
const el = document.getElementById('pager');
const p = r.page, pages = r.pages;
if (pages <= 1 && r.total <= 0){ el.innerHTML = ''; return; }
const q = '?page=' + p; // 当前页参数(保留给表单用,实际分页走 JS)
let html = '<select id="f_page_size" onchange="loadPage(1)" style="padding:6px 8px;border:1px solid #d1d5db;border-radius:8px;font-size:13px;">' +
[10,20,50,100].map(n => `<option value="${n}" ${n==CUR.page_size?'selected':''}>每页 ${n} 条</option>`).join('') + '</select>';
html += `<button class="btn mini" ${p<=1?'disabled':''} onclick="loadPage(1)">⏮ 首页</button>`;
html += `<button class="btn mini" ${p<=1?'disabled':''} onclick="loadPage(${p-1})">上一页</button>`;
html += `<span class="muted">第 ${p} / ${pages} 页</span>`;
html += `<button class="btn mini" ${p>=pages?'disabled':''} onclick="loadPage(${p+1})">下一页</button>`;
html += `<button class="btn mini" ${p>=pages?'disabled':''} onclick="loadPage(${pages})">末页 ⏭</button>`;
html += `<span class="muted">跳转</span><input id="jump_page" type="number" min="1" max="${pages}" value="${p}" style="width:70px;padding:6px;border:1px solid #d1d5db;border-radius:8px;">`;
html += `<button class="btn mini" onclick="jump()">GO</button>`;
el.innerHTML = html;
}
function jump(){
const v = parseInt(document.getElementById('jump_page').value) || 1;
loadPage(Math.max(1, Math.min(v, CUR.pages)));
}
function resetFilter(){
['f_type','f_status','f_q'].forEach(id => document.getElementById(id).value = '');
loadPage(1);
}
// ---------- 选中 / 批量删除 ----------
function updSel(){
const cbs = Array.from(document.querySelectorAll('.row_check'));
const n = cbs.filter(c => c.checked).length;
document.getElementById('sel_count').textContent = n;
const all = document.getElementById('check_all');
if (all){ all.checked = cbs.length > 0 && n === cbs.length; }
}
function toggleAll(el){
document.querySelectorAll('.row_check').forEach(c => c.checked = el.checked);
updSel();
}
function selectedIds(){
return Array.from(document.querySelectorAll('.row_check:checked'))
.map(c => parseInt(c.closest('tr').dataset.lid)).filter(Boolean);
}
async function delSelected(){
const ids = selectedIds();
if (!ids.length){ toast('请先勾选要删除的日志', false); return; }
if (!confirm(`确认删除选中的 ${ids.length} 条日志?`)) return;
const r = await API.json('/api/logs/delete', {ids});
toast(r.ok ? `✅ 已删除 ${r.deleted} 条` : '❌ 删除失败', r.ok);
if (r.ok) loadPage(CUR.page);
}
async function delFiltered(){
const q = {
type: document.getElementById('f_type').value,
status: document.getElementById('f_status').value,
q: document.getElementById('f_q').value
};
if (!confirm('确认删除当前筛选条件下的全部日志?此操作不可恢复!')) return;
const r = await API.json('/api/logs/delete', {all:true, ...q});
toast(r.ok ? `✅ 已删除 ${r.deleted} 条` : '❌ 删除失败', r.ok);
if (r.ok) loadPage(1);
}
async function delOne(id){
if (!confirm('确认删除该条日志?')) return;
const r = await API.json('/api/logs/delete', {ids:[id]});
toast(r.ok ? '✅ 已删除' : '❌ 删除失败', r.ok);
if (r.ok) loadPage(CUR.page);
}
// ---------- 查看通知内容 ----------
async function viewLog(id){
const row = document.querySelector(`tr[data-lid="${id}"]`);
const subject = row ? row.querySelector('td a').textContent : ('日志 #' + id);
const r = await API.get('/api/logs/' + id);
document.getElementById('viewTitle').textContent = '📄 ' + subject;
const vb = document.getElementById('viewBody');
if (r.ok && r.log && r.log.body){
vb.innerHTML = '<iframe id="viewFrame" style="width:100%;height:62vh;border:1px solid #e5e7eb;border-radius:8px;"></iframe>';
document.getElementById('viewFrame').srcdoc = r.log.body;
} else if (r.ok && r.log && r.log.article_ids && r.log.article_ids.length){
vb.innerHTML = '<div class="muted small" style="margin-bottom:8px;">该条为旧记录(未存正文),关联资讯:</div><div id="art_links"></div>';
const art = await API.get('/api/articles/batch', {ids: r.log.article_ids.join(',')});
document.getElementById('art_links').innerHTML = (art.ok && art.articles.length)
? art.articles.map(a => `<div style="padding:6px 0;border-bottom:1px solid #f3f4f6;"><a href="/news/${a.id}" target="_blank">${esc(a.title)}</a> <span class="muted small">#${a.id}</span></div>`).join('')
: '<div class="empty">无关联资讯</div>';
} else {
vb.innerHTML = '<div class="empty">该条记录未保存正文内容</div>' +
(r.ok && r.log && r.log.detail ? `<div class="muted" style="margin-top:8px;">${esc(r.log.detail)}</div>` : '');
}
document.getElementById('viewModal').style.display = 'flex';
}
async function testMail(){
const r = await API.json('/api/actions', {action:'test_mail'});
toast(r.ok ? '✅ 测试邮件已发送' : ('❌ ' + r.error), r.ok);
}
async function testErrorMail(){
const r = await API.json('/api/actions', {action:'test_error_mail'});
toast(r.ok ? '✅ 测试错误通知邮件已发送' : ('❌ ' + r.error), r.ok);
}
loadPage(1);
</script>
{% endblock %}