212 lines
11 KiB
HTML
212 lines
11 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}论文管理 — HF Daily Papers{% endblock %}
|
|
{% block content %}
|
|
<div class="admin-page">
|
|
{% set active = "papers" %}{% include "partials/admin_subnav.html" %}
|
|
|
|
<h1 class="page-heading">📄 论文管理</h1>
|
|
|
|
<!-- 搜索和筛选 -->
|
|
<form class="paper-search-form" method="get" action="/admin/papers">
|
|
<div class="paper-search-row">
|
|
<input type="text" name="q" value="{{ request.query_params.get('q', '') }}"
|
|
placeholder="搜索标题 / 摘要..." class="paper-search-input" />
|
|
<input type="text" name="date_from" value="{{ request.query_params.get('date_from', '') }}"
|
|
class="paper-filter-input kami-date-input" placeholder="起始日期" readonly id="date-from-trigger" />
|
|
<input type="text" name="date_to" value="{{ request.query_params.get('date_to', '') }}"
|
|
class="paper-filter-input kami-date-input" placeholder="结束日期" readonly id="date-to-trigger" />
|
|
<select name="summary_status" class="paper-filter-input">
|
|
<option value="all" {% if current_status == 'all' %}selected{% endif %}>全部状态</option>
|
|
<option value="none" {% if current_status == 'none' %}selected{% endif %}>未总结</option>
|
|
<option value="done" {% if current_status == 'done' %}selected{% endif %}>已完成</option>
|
|
<option value="pending" {% if current_status == 'pending' %}selected{% endif %}>待总结</option>
|
|
<option value="failed" {% if current_status == 'failed' %}selected{% endif %}>失败</option>
|
|
</select>
|
|
<select name="sort" class="paper-filter-input">
|
|
<option value="date_desc" {% if current_sort == 'date_desc' %}selected{% endif %}>日期 ↓</option>
|
|
<option value="date_asc" {% if current_sort == 'date_asc' %}selected{% endif %}>日期 ↑</option>
|
|
<option value="upvotes_desc" {% if current_sort == 'upvotes_desc' %}selected{% endif %}>Upvotes ↓</option>
|
|
<option value="title_asc" {% if current_sort == 'title_asc' %}selected{% endif %}>标题 A→Z</option>
|
|
</select>
|
|
<button type="submit" class="paper-search-btn">搜索</button>
|
|
<a class="admin-action-btn admin-action-btn-sm" href="/admin/papers/export.csv{% if request.query_params %}?{{ request.query_params }}{% endif %}">⬇ 导出 CSV</a>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- 批量操作栏 -->
|
|
<div class="paper-batch-bar">
|
|
<span class="paper-batch-label">批量操作</span>
|
|
<span class="paper-selected-count" id="selected-count">已选 0 篇</span>
|
|
<button class="admin-action-btn admin-action-btn-sm" onclick="batchAction('summarize')" id="batch-summarize-btn" disabled>📝 批量总结</button>
|
|
<button class="admin-action-btn admin-action-btn-sm" onclick="batchAction('recrawl')" id="batch-recrawl-btn" disabled>🔄 批量重抓</button>
|
|
<button class="admin-action-btn admin-action-btn-sm admin-action-btn-danger" onclick="batchAction('delete')" id="batch-delete-btn" disabled>🗑 批量删除</button>
|
|
</div>
|
|
|
|
{% if papers %}
|
|
<div class="admin-table-wrap">
|
|
<table class="admin-table paper-manage-table">
|
|
<thead>
|
|
<tr>
|
|
<th class="th-check"><input type="checkbox" class="admin-check" id="select-all" onchange="toggleSelectAll(this)" /></th>
|
|
<th>标题</th>
|
|
<th>日期</th>
|
|
<th>👍</th>
|
|
<th>状态</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for paper in papers %}
|
|
<tr data-arxiv="{{ paper.arxiv_id }}">
|
|
<td><input type="checkbox" class="admin-check paper-check" value="{{ paper.arxiv_id }}" onchange="updateSelectedCount()" /></td>
|
|
<td class="title-cell">
|
|
<a href="/paper/{{ paper.arxiv_id }}" target="_blank">
|
|
{{ (paper.title_zh or paper.title_en)[:70] }}{% if (paper.title_zh or paper.title_en)|length > 70 %}...{% endif %}
|
|
</a>
|
|
</td>
|
|
<td class="time-cell">{{ paper.paper_date.strftime('%Y-%m-%d') if paper.paper_date else '-' }}</td>
|
|
<td>{{ paper.upvotes or 0 }}</td>
|
|
<td>
|
|
{% set st = paper_summary_statuses.get(paper.arxiv_id, 'none') %}
|
|
<span class="status-badge status-{{ 'success' if st == 'done' else ('running' if st in ['pending', 'processing'] else 'failed') }}">
|
|
{% if st == 'done' %}✓{% elif st == 'pending' %}⏳{% elif st == 'processing' %}⟳{% elif st in ['failed', 'permanent_failure'] %}✗{% else %}○{% endif %}
|
|
</span>
|
|
</td>
|
|
<td class="action-cell">
|
|
<button class="action-btn-sm" title="重新总结" onclick="retryOne('{{ paper.arxiv_id }}', this)">↻</button>
|
|
<button class="action-btn-sm" title="重新抓取元数据" onclick="recrawlOne('{{ paper.arxiv_id }}', this)">🔄</button>
|
|
<button class="action-btn-sm action-btn-danger" title="删除" onclick="confirmDeleteSingle('{{ paper.arxiv_id }}', '{{ (paper.title_zh or paper.title_en)[:40] | replace("'", "\\'") }}')">🗑</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{% set total_pages = ((total + per_page - 1) // per_page) if total else 1 %}
|
|
{% if total_pages > 1 %}
|
|
<div class="pagination">
|
|
{% if page > 1 %}
|
|
<a class="page-btn" href="{{ pagination_url(page - 1) }}">← 上一页</a>
|
|
{% endif %}
|
|
<span class="page-info">第 {{ page }} / {{ total_pages }} 页(共 {{ total }} 篇)</span>
|
|
{% if page < total_pages %}
|
|
<a class="page-btn" href="{{ pagination_url(page + 1) }}">下一页 →</a>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>没有找到匹配的论文</p>
|
|
<p class="hint">调整搜索条件或清除筛选。</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- 删除确认弹窗 -->
|
|
<div class="confirm-overlay" id="confirm-overlay" style="display:none;">
|
|
<div class="confirm-dialog">
|
|
<p class="confirm-msg" id="confirm-msg">确定删除?</p>
|
|
<div class="confirm-actions">
|
|
<button class="confirm-btn confirm-btn-cancel" onclick="closeConfirm()">取消</button>
|
|
<button class="confirm-btn confirm-btn-ok" id="confirm-ok" onclick="doConfirmAction()">确定删除</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
let _confirmAction=null, _confirmTarget=null;
|
|
|
|
function toggleSelectAll(el) {
|
|
document.querySelectorAll('.paper-check').forEach(c=>{c.checked=el.checked;});
|
|
updateSelectedCount();
|
|
}
|
|
function updateSelectedCount() {
|
|
const n=document.querySelectorAll('.paper-check:checked').length;
|
|
document.getElementById('selected-count').textContent='已选 '+n+' 篇';
|
|
document.getElementById('batch-summarize-btn').disabled=n===0;
|
|
document.getElementById('batch-recrawl-btn').disabled=n===0;
|
|
document.getElementById('batch-delete-btn').disabled=n===0;
|
|
}
|
|
function retryOne(arxivId,btn) {
|
|
btn.disabled=true;btn.textContent='...';
|
|
fetch('/admin/summarize/'+arxivId,{method:'POST',headers:{'Content-Type':'application/json'}})
|
|
.then(r=>r.json())
|
|
.then(data=>showToast(data.error?'❌ '+data.error.substring(0,100):'✅ 已提交重试'))
|
|
.catch(()=>showToast('❌ 请求失败'))
|
|
.finally(()=>{btn.disabled=false;btn.textContent='↻';});
|
|
}
|
|
function recrawlOne(arxivId,btn) {
|
|
btn.disabled=true;btn.textContent='...';
|
|
fetch('/admin/paper-recrawl/'+arxivId,{method:'POST',headers:{'Content-Type':'application/json'}})
|
|
.then(r=>r.json())
|
|
.then(data=>showToast(data.error?'❌ '+data.error.substring(0,100):'✅ 重抓任务已创建,可在任务页查看'))
|
|
.catch(()=>showToast('❌ 请求失败'))
|
|
.finally(()=>{btn.disabled=false;btn.textContent='🔄';});
|
|
}
|
|
function confirmDeleteSingle(arxivId,title) {
|
|
document.getElementById('confirm-msg').textContent='确定删除论文「'+title+'」?此操作不可恢复。';
|
|
_confirmAction='delete-single'; _confirmTarget=arxivId;
|
|
document.getElementById('confirm-overlay').style.display='flex';
|
|
}
|
|
function batchAction(action) {
|
|
const ids=Array.from(document.querySelectorAll('.paper-check:checked')).map(c=>c.value);
|
|
if(!ids.length)return;
|
|
if(action==='delete'){
|
|
document.getElementById('confirm-msg').textContent='确定删除 '+ids.length+' 篇论文?此操作不可恢复。';
|
|
_confirmAction='batch-delete'; _confirmTarget=ids;
|
|
document.getElementById('confirm-overlay').style.display='flex';
|
|
} else if(action==='summarize'){
|
|
fetch('/admin/papers-batch-action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'summarize',arxiv_ids:ids})})
|
|
.then(r=>r.json())
|
|
.then(data=>showToast(data.error?'❌ '+data.error.substring(0,100):'✅ 已提交批量总结'))
|
|
.catch(()=>showToast('❌ 请求失败'));
|
|
} else if(action==='recrawl'){
|
|
fetch('/admin/papers-batch-action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'recrawl',arxiv_ids:ids})})
|
|
.then(r=>r.json())
|
|
.then(data=>showToast(data.error?'❌ '+data.error.substring(0,100):'✅ 已提交批量重抓,可在任务页查看'))
|
|
.catch(()=>showToast('❌ 请求失败'));
|
|
}
|
|
}
|
|
function doConfirmAction() {
|
|
if(_confirmAction==='delete-single'){
|
|
fetch('/admin/paper-delete/'+_confirmTarget,{method:'POST',headers:{'Content-Type':'application/json'}})
|
|
.then(r=>r.json()).then(data=>{showToast(data.error?'❌ '+data.error.substring(0,100):'✅ 已删除');setTimeout(()=>location.reload(),1000);})
|
|
.catch(()=>showToast('❌ 请求失败'));
|
|
} else if(_confirmAction==='batch-delete'){
|
|
fetch('/admin/papers-batch-action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'delete',arxiv_ids:_confirmTarget})})
|
|
.then(r=>r.json()).then(data=>{showToast(data.error?'❌ '+data.error.substring(0,100):'✅ 已删除');setTimeout(()=>location.reload(),1000);})
|
|
.catch(()=>showToast('❌ 请求失败'));
|
|
}
|
|
closeConfirm();
|
|
}
|
|
function closeConfirm() { document.getElementById('confirm-overlay').style.display='none'; _confirmAction=null; _confirmTarget=null; }
|
|
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeConfirm();});
|
|
</script>
|
|
<script src="/static/js/date-picker.js"></script>
|
|
<script>
|
|
(function() {
|
|
var today = new Date().toISOString().slice(0, 10);
|
|
var fromEl = document.getElementById('date-from-trigger');
|
|
var toEl = document.getElementById('date-to-trigger');
|
|
|
|
if (fromEl) {
|
|
new KamiDatePicker(fromEl, {
|
|
value: fromEl.value || null,
|
|
maxDate: today,
|
|
onChange: function(dateStr) { fromEl.value = dateStr || ''; }
|
|
});
|
|
}
|
|
if (toEl) {
|
|
new KamiDatePicker(toEl, {
|
|
value: toEl.value || null,
|
|
maxDate: today,
|
|
onChange: function(dateStr) { toEl.value = dateStr || ''; }
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
{% endblock %}
|