221 lines
10 KiB
HTML
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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 %}
|