feat: add admin dashboard, pipeline service, lightbox, and update dependencies
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
{# Admin subnav — 管理后台三个页面共享。active 参数: "dashboard" / "papers" / "logs" #}
|
||||
<nav class="admin-subnav">
|
||||
<a href="/admin/" class="admin-subnav-link {{ 'active' if active == 'dashboard' else '' }}">仪表盘</a>
|
||||
<a href="/admin/papers" class="admin-subnav-link {{ 'active' if active == 'papers' else '' }}">论文管理</a>
|
||||
<a href="/admin/logs" class="admin-subnav-link {{ 'active' if active == 'logs' else '' }}">日志</a>
|
||||
<span class="admin-subnav-spacer"></span>
|
||||
<form action="/admin/logout" method="post" class="admin-subnav-form">
|
||||
<button type="submit" class="admin-subnav-link admin-subnav-logout">退出登录</button>
|
||||
</form>
|
||||
</nav>
|
||||
@@ -1,15 +1,45 @@
|
||||
{# 论文卡片组件 — paper 变量必须在上下文中 #}
|
||||
<article class="paper-card" data-arxiv="{{ paper.arxiv_id }}">
|
||||
{# 论文卡片组件 — 支持普通和搜索两种模式 #}
|
||||
|
||||
{% macro render_card(paper, snippets=None, distances=None, variant="default") %}
|
||||
<article class="paper-card {% if variant == 'search' %}search-result{% endif %}"
|
||||
data-arxiv="{{ paper.arxiv_id }}">
|
||||
<div class="paper-card-header">
|
||||
<h2 class="paper-title">
|
||||
<a href="/paper/{{ paper.arxiv_id }}">
|
||||
{{ paper.title_zh or paper.title_en }}
|
||||
{% if variant == 'search' and snippets %}
|
||||
{% set snip = snippets.get(paper.id, {}) %}
|
||||
{% if snip and snip.title_zh %}
|
||||
{{ snip.title_zh | safe }}
|
||||
{% elif paper.title_zh %}
|
||||
{{ paper.title_zh }}
|
||||
{% else %}
|
||||
{{ paper.title_en }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ paper.title_zh or paper.title_en }}
|
||||
{% endif %}
|
||||
</a>
|
||||
</h2>
|
||||
<span class="paper-upvotes">👍 {{ paper.upvotes }}</span>
|
||||
{% if variant == 'search' and distances and paper.arxiv_id in distances %}
|
||||
<span class="similarity-score" title="语义相似度距离">
|
||||
🎯 {{ "%.3f"|format(distances[paper.arxiv_id]) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if paper.summary and paper.summary.one_line %}
|
||||
{% if variant == 'search' and snippets %}
|
||||
{% set snip = snippets.get(paper.id, {}) %}
|
||||
{% if snip and snip.abstract %}
|
||||
<p class="paper-snippet">{{ snip.abstract | safe }}</p>
|
||||
{% elif paper.summary and paper.summary.one_line %}
|
||||
<p class="paper-one-line">{{ paper.summary.one_line }}</p>
|
||||
{% elif paper.abstract %}
|
||||
<p class="paper-abstract-preview">
|
||||
{{ paper.abstract[:200] }}{% if paper.abstract|length > 200 %}…{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% elif paper.summary and paper.summary.one_line %}
|
||||
<p class="paper-one-line">{{ paper.summary.one_line }}</p>
|
||||
{% elif paper.abstract %}
|
||||
<p class="paper-abstract-preview">
|
||||
@@ -21,6 +51,9 @@
|
||||
<span class="paper-authors">
|
||||
{{ paper.authors|map(attribute='name')|join(', ')|truncate(80) }}
|
||||
</span>
|
||||
{% if variant == 'search' %}
|
||||
<span class="paper-date">{{ paper.paper_date }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="paper-tags">
|
||||
@@ -39,14 +72,14 @@
|
||||
未总结
|
||||
{% elif paper.summary_status.status == 'processing' %}
|
||||
🔄 总结中
|
||||
{% elif paper.summary_status.status == 'failed' or paper.summary_status.status == 'permanent_failure' %}
|
||||
{% elif paper.summary_status.status in ('failed', 'permanent_failure') %}
|
||||
❌ 总结失败
|
||||
{% elif paper.summary_status.status == 'done' %}
|
||||
✅ 已总结
|
||||
{% endif %}
|
||||
{# djlint:on #}
|
||||
</span>
|
||||
{% if paper.reading_status %}
|
||||
{% if paper.reading_status and variant != 'search' %}
|
||||
<span class="reading-badge reading-{{ paper.reading_status.status }}">
|
||||
{# djlint:off #}
|
||||
{% if paper.reading_status.status == 'unread' %}
|
||||
@@ -63,6 +96,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="paper-footer-right">
|
||||
{% if variant != 'search' %}
|
||||
<button
|
||||
class="btn-bookmark {% if paper.bookmark %}active{% endif %}"
|
||||
hx-post="/api/bookmark/{{ paper.arxiv_id }}"
|
||||
@@ -71,9 +105,12 @@
|
||||
>
|
||||
{% if paper.bookmark %}★{% else %}☆{% endif %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<a href="/paper/{{ paper.arxiv_id }}" class="btn-detail">详情 →</a>
|
||||
</div>
|
||||
</div>
|
||||
{# HTMX 刷新锚点 — button swap 替换此 div #}
|
||||
{% if variant != 'search' %}
|
||||
<span id="user-data-{{ paper.arxiv_id }}"></span>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<!-- 总结状态列表(HTMX 片段) -->
|
||||
{% if results %}
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table summary-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>标题</th>
|
||||
<th>日期</th>
|
||||
<th>状态</th>
|
||||
<th>重试</th>
|
||||
<th>错误类型</th>
|
||||
<th>错误信息</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for paper, ss in results %}
|
||||
<tr>
|
||||
<td class="title-cell">
|
||||
<a href="/paper/{{ paper.arxiv_id }}" target="_blank">
|
||||
{{ (paper.title_zh or paper.title_en)[:60] }}{% if (paper.title_zh or paper.title_en)|length > 60 %}...{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td class="time-cell">{{ paper.paper_date.strftime('%m-%d') if paper.paper_date else '-' }}</td>
|
||||
<td>
|
||||
{% set st = ss.status if ss else '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 == 'failed' %}✗ 失败
|
||||
{% elif st == 'permanent_failure' %}✗ 永久失败
|
||||
{% else %}○ 未开始{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ ss.retry_count if ss else 0 }}</td>
|
||||
<td>{{ (ss.error_type or '-') if ss else '-' }}</td>
|
||||
<td class="error-cell" title="{{ ss.error if ss else '' }}">
|
||||
{% if ss and ss.error %}
|
||||
{{ ss.error[:60] + '...' if ss.error|length > 60 else ss.error }}
|
||||
{% else %}-{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if st in ['failed', 'permanent_failure', 'pending', 'none'] %}
|
||||
<button class="retry-btn" onclick="retrySummary('{{ paper.arxiv_id }}', this)">重试</button>
|
||||
{% else %}
|
||||
<span style="color: var(--ink-muted); font-size: 0.75rem;">-</span>
|
||||
{% endif %}
|
||||
</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 %}
|
||||
<button class="page-btn" onclick="summaryPage({{ page - 1 }})">← 上一页</button>
|
||||
{% endif %}
|
||||
<span class="page-info">第 {{ page }} / {{ total_pages }} 页(共 {{ total }} 篇)</span>
|
||||
{% if page < total_pages %}
|
||||
<button class="page-btn" onclick="summaryPage({{ page + 1 }})">下一页 →</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function summaryPage(p) {
|
||||
const status = document.querySelector('.summary-filters .filter-chip.active')?.dataset.status || 'all';
|
||||
htmx.ajax('GET', '/admin/summary-status?status=' + status + '&page=' + p, '#summary-list');
|
||||
}
|
||||
</script>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<p>无匹配结果</p>
|
||||
<p class="hint">调整筛选条件或触发总结任务。</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user