feat: add admin dashboard, pipeline service, lightbox, and update dependencies
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}管理仪表盘 — HF Daily Papers{% endblock %}
|
||||
{% block content %}
|
||||
<div class="admin-page">
|
||||
{% set active = "dashboard" %}{% include "partials/admin_subnav.html" %}
|
||||
|
||||
<h1 class="page-heading">📊 系统状态</h1>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.total_papers }}</div>
|
||||
<div class="stat-label">论文总数</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.today_papers }}</div>
|
||||
<div class="stat-label">今日新增</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value {% if stats.pending_count > 0 %}stat-warn{% endif %}">
|
||||
{{ stats.pending_count + stats.none_count }}
|
||||
</div>
|
||||
<div class="stat-label">待总结</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value {% if stats.failed_count > 0 %}stat-danger{% endif %}">
|
||||
{{ stats.failed_count }}
|
||||
</div>
|
||||
<div class="stat-label">总结失败</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-quick-actions">
|
||||
<button class="admin-action-btn" onclick="adminAction('crawl')">🔄 抓取今天</button>
|
||||
<button class="admin-action-btn" onclick="adminAction('summarize')">📝 批量总结</button>
|
||||
<button class="admin-action-btn" onclick="adminAction('cleanup')">🧹 清理临时文件</button>
|
||||
</div>
|
||||
|
||||
<div class="admin-info-grid">
|
||||
<div class="admin-info-card">
|
||||
<h2 class="admin-info-title">🕐 调度器</h2>
|
||||
<div class="admin-info-body">
|
||||
<div class="info-row">
|
||||
<span class="info-label">状态</span>
|
||||
<span class="info-value">
|
||||
{% if stats.scheduler_enabled %}
|
||||
<span class="status-dot status-dot-on"></span> 运行中
|
||||
{% else %}
|
||||
<span class="status-dot status-dot-off"></span> 未启用
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">调度时间</span>
|
||||
<span class="info-value">{{ stats.schedule_time }}({{ stats.timezone }})</span>
|
||||
</div>
|
||||
{% if stats.next_run %}
|
||||
<div class="info-row">
|
||||
<span class="info-label">下次执行</span>
|
||||
<span class="info-value">{{ stats.next_run[:19] | replace('T', ' ') }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if stats.active_locks %}
|
||||
<div class="info-row">
|
||||
<span class="info-label">活跃任务</span>
|
||||
<span class="info-value">
|
||||
{% for lock in stats.active_locks %}
|
||||
<span class="task-badge task-{{ lock.task }}">{{ lock.task }}</span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="info-row">
|
||||
<span class="info-label"></span>
|
||||
<button class="admin-action-btn admin-action-btn-sm" onclick="triggerPipeline()">
|
||||
▶ 立即执行流水线
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scheduler-history">
|
||||
<h3 class="section-subtitle">执行历史</h3>
|
||||
{% if scheduler_history %}
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table admin-table-compact">
|
||||
<thead>
|
||||
<tr><th>时间</th><th>状态</th><th>发现</th><th>新增</th><th>错误</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in scheduler_history %}
|
||||
<tr>
|
||||
<td class="time-cell">{{ log.started_at.strftime('%m-%d %H:%M') if log.started_at else '-' }}</td>
|
||||
<td><span class="status-badge status-{{ log.status }}">
|
||||
{% if log.status == 'success' %}✓{% elif log.status == 'running' %}⟳{% elif log.status == 'failed' %}✗{% else %}{{ log.status }}{% endif %}
|
||||
</span></td>
|
||||
<td>{{ log.papers_found or 0 }}</td>
|
||||
<td>{{ log.papers_new or 0 }}</td>
|
||||
<td class="error-cell" title="{{ log.error or '' }}">
|
||||
{{ (log.error[:50] + '...') if log.error and log.error|length > 50 else (log.error or '-') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="hint">暂无调度器执行记录。</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-info-card">
|
||||
<h2 class="admin-info-title">💾 存储概况</h2>
|
||||
<div class="admin-info-body">
|
||||
<div class="info-row"><span class="info-label">数据库</span><span class="info-value">{{ stats.db_size }}</span></div>
|
||||
<div class="info-row"><span class="info-label">论文文件</span><span class="info-value">{{ stats.papers_size }}</span></div>
|
||||
<div class="info-row"><span class="info-label">临时文件</span><span class="info-value">{{ stats.tmp_size }}</span></div>
|
||||
</div>
|
||||
<div class="summary-dist">
|
||||
<h3 class="section-subtitle">总结状态分布</h3>
|
||||
<div class="summary-dist-bars">
|
||||
{% set total = stats.total_papers or 1 %}
|
||||
{% set labels = {"done": "已完成", "pending": "待总结", "running": "运行中", "processing": "处理中", "failed": "失败", "permanent_failure": "永久失败", "none": "未开始"} %}
|
||||
{% for st, cnt in stats.status_counts.items() %}
|
||||
{% if cnt > 0 %}
|
||||
<div class="dist-row">
|
||||
<span class="dist-label">{{ labels.get(st, st) }}</span>
|
||||
<div class="dist-bar-wrap"><div class="dist-bar dist-bar-{{ st }}" style="width: {{ (cnt / total * 100)|round(1) }}%"></div></div>
|
||||
<span class="dist-count">{{ cnt }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-section">
|
||||
<h2 class="admin-section-title">📋 最近活动</h2>
|
||||
{% if stats.recent_logs %}
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr><th>任务</th><th>状态</th><th>日期</th><th>发现</th><th>新增</th><th>开始时间</th><th>完成时间</th><th>错误</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in stats.recent_logs %}
|
||||
<tr>
|
||||
<td><span class="task-badge task-{{ log.task }}">{{ log.task }}</span></td>
|
||||
<td><span class="status-badge status-{{ log.status }}">
|
||||
{# djlint:off #}
|
||||
{% if log.status == 'success' %}✓ 成功{% elif log.status == 'running' %}⟳ 运行中{% elif log.status == 'failed' %}✗ 失败{% else %}{{ log.status }}{% endif %}
|
||||
{# djlint:on #}
|
||||
</span></td>
|
||||
<td>{{ log.date or '-' }}</td>
|
||||
<td>{{ log.papers_found or 0 }}</td>
|
||||
<td>{{ log.papers_new or 0 }}</td>
|
||||
<td class="time-cell">{{ log.started_at.strftime('%m-%d %H:%M') if log.started_at else '-' }}</td>
|
||||
<td class="time-cell">{{ log.completed_at.strftime('%m-%d %H:%M') if log.completed_at else '-' }}</td>
|
||||
<td class="error-cell" title="{{ log.error or '' }}">
|
||||
{{ (log.error[:60] + '...') if log.error and log.error|length > 60 else (log.error or '-') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<p>暂无活动日志</p>
|
||||
<p class="hint">通过快捷操作触发任务后,日志将出现在这里。</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function triggerPipeline() {
|
||||
fetch("/admin/trigger-pipeline", { method: "POST", headers: { "Content-Type": "application/json" } })
|
||||
.then(r => { if (r.status===303||r.status===401) { window.location.href="/admin/login"; return; } return r.json(); })
|
||||
.then(data => { if (data) showToast(data.error ? "❌ " + data.error.substring(0,200) : "✅ 流水线已触发"); })
|
||||
.catch(err => showToast("❌ 请求失败"));
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
+89
-319
@@ -1,68 +1,43 @@
|
||||
{% extends "base.html" %} {% block title %}管理日志 — HF Daily Papers{% endblock
|
||||
%} {% block content %}
|
||||
<div class="admin-logs-page">
|
||||
{% extends "base.html" %}
|
||||
{% block title %}管理日志 — HF Daily Papers{% endblock %}
|
||||
{% block content %}
|
||||
<div class="admin-page">
|
||||
{% set active = "logs" %}{% include "partials/admin_subnav.html" %}
|
||||
|
||||
<h1 class="page-heading">📋 管理日志</h1>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<div class="admin-tabs">
|
||||
<button class="admin-tab active" data-tab="crawl-logs">抓取日志</button>
|
||||
<button class="admin-tab" data-tab="delete-jobs">删除记录</button>
|
||||
<button class="admin-tab" data-tab="summary-status">总结状态</button>
|
||||
</div>
|
||||
|
||||
<!-- 抓取日志 Tab -->
|
||||
<!-- 抓取日志 -->
|
||||
<div class="admin-tab-content active" id="crawl-logs">
|
||||
{% if crawl_logs %}
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>任务</th>
|
||||
<th>状态</th>
|
||||
<th>日期</th>
|
||||
<th>发现</th>
|
||||
<th>新增</th>
|
||||
<th>开始时间</th>
|
||||
<th>完成时间</th>
|
||||
<th>错误</th>
|
||||
</tr>
|
||||
<tr><th>ID</th><th>任务</th><th>状态</th><th>日期</th><th>发现</th><th>新增</th><th>开始时间</th><th>完成时间</th><th>错误</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in crawl_logs %}
|
||||
<tr>
|
||||
<td>{{ log.id }}</td>
|
||||
<td>
|
||||
<span class="task-badge task-{{ log.task }}">{{ log.task }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge status-{{ log.status }}">
|
||||
{# djlint:off #}
|
||||
{% if log.status == 'success' %}
|
||||
✓ 成功
|
||||
{% elif log.status == 'running' %}
|
||||
⟳ 运行中
|
||||
{% elif log.status == 'failed' %}
|
||||
✗ 失败
|
||||
{% else %}
|
||||
{{ log.status }}
|
||||
{% endif %}
|
||||
{# djlint:on #}
|
||||
</span>
|
||||
</td>
|
||||
<td><span class="task-badge task-{{ log.task }}">{{ log.task }}</span></td>
|
||||
<td><span class="status-badge status-{{ log.status }}">
|
||||
{# djlint:off #}
|
||||
{% if log.status == 'success' %}✓ 成功{% elif log.status == 'running' %}⟳ 运行中{% elif log.status == 'failed' %}✗ 失败{% else %}{{ log.status }}{% endif %}
|
||||
{# djlint:on #}
|
||||
</span></td>
|
||||
<td>{{ log.date or '-' }}</td>
|
||||
<td>{{ log.papers_found or 0 }}</td>
|
||||
<td>{{ log.papers_new or 0 }}</td>
|
||||
<td class="time-cell">
|
||||
{{ log.started_at.strftime('%m-%d %H:%M') if log.started_at else
|
||||
'-' }}
|
||||
</td>
|
||||
<td class="time-cell">
|
||||
{{ log.completed_at.strftime('%m-%d %H:%M') if log.completed_at
|
||||
else '-' }}
|
||||
</td>
|
||||
<td class="time-cell">{{ log.started_at.strftime('%m-%d %H:%M') if log.started_at else '-' }}</td>
|
||||
<td class="time-cell">{{ log.completed_at.strftime('%m-%d %H:%M') if log.completed_at else '-' }}</td>
|
||||
<td class="error-cell" title="{{ log.error or '' }}">
|
||||
{{ log.error[:80] + '...' if log.error and log.error|length > 80
|
||||
else (log.error or '-') }}
|
||||
{{ log.error[:80] + '...' if log.error and log.error|length > 80 else (log.error or '-') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -77,23 +52,13 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 删除记录 Tab -->
|
||||
<!-- 删除记录 -->
|
||||
<div class="admin-tab-content" id="delete-jobs">
|
||||
{% if delete_jobs %}
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>起始日期</th>
|
||||
<th>结束日期</th>
|
||||
<th>包含笔记</th>
|
||||
<th>论文数</th>
|
||||
<th>状态</th>
|
||||
<th>开始时间</th>
|
||||
<th>完成时间</th>
|
||||
<th>错误</th>
|
||||
</tr>
|
||||
<tr><th>ID</th><th>起始日期</th><th>结束日期</th><th>包含笔记</th><th>论文数</th><th>状态</th><th>开始时间</th><th>完成时间</th><th>错误</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for job in delete_jobs %}
|
||||
@@ -103,32 +68,15 @@
|
||||
<td>{{ job.date_end }}</td>
|
||||
<td>{{ '是' if job.include_notes else '否' }}</td>
|
||||
<td>{{ job.paper_count or 0 }}</td>
|
||||
<td>
|
||||
<span class="status-badge status-{{ job.status }}">
|
||||
{# djlint:off #}
|
||||
{% if job.status == 'success' %}
|
||||
✓ 成功
|
||||
{% elif job.status == 'running' %}
|
||||
⟳ 运行中
|
||||
{% elif job.status == 'failed' %}
|
||||
✗ 失败
|
||||
{% else %}
|
||||
{{ job.status }}
|
||||
{% endif %}
|
||||
{# djlint:on #}
|
||||
</span>
|
||||
</td>
|
||||
<td class="time-cell">
|
||||
{{ job.started_at.strftime('%m-%d %H:%M') if job.started_at else
|
||||
'-' }}
|
||||
</td>
|
||||
<td class="time-cell">
|
||||
{{ job.completed_at.strftime('%m-%d %H:%M') if job.completed_at
|
||||
else '-' }}
|
||||
</td>
|
||||
<td><span class="status-badge status-{{ job.status }}">
|
||||
{# djlint:off #}
|
||||
{% if job.status == 'success' %}✓ 成功{% elif job.status == 'running' %}⟳ 运行中{% elif job.status == 'failed' %}✗ 失败{% else %}{{ job.status }}{% endif %}
|
||||
{# djlint:on #}
|
||||
</span></td>
|
||||
<td class="time-cell">{{ job.started_at.strftime('%m-%d %H:%M') if job.started_at else '-' }}</td>
|
||||
<td class="time-cell">{{ job.completed_at.strftime('%m-%d %H:%M') if job.completed_at else '-' }}</td>
|
||||
<td class="error-cell" title="{{ job.error or '' }}">
|
||||
{{ job.error[:80] + '...' if job.error and job.error|length > 80
|
||||
else (job.error or '-') }}
|
||||
{{ job.error[:80] + '...' if job.error and job.error|length > 80 else (job.error or '-') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -143,259 +91,81 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 总结状态 -->
|
||||
<div class="admin-tab-content" id="summary-status">
|
||||
<div class="summary-filters">
|
||||
<span class="summary-filter-label">筛选:</span>
|
||||
<button class="filter-chip active" data-status="all">全部</button>
|
||||
<button class="filter-chip" data-status="none">未开始</button>
|
||||
<button class="filter-chip" data-status="pending">待总结</button>
|
||||
<button class="filter-chip" data-status="processing">运行中</button>
|
||||
<button class="filter-chip" data-status="failed">失败</button>
|
||||
<button class="filter-chip" data-status="permanent_failure">永久失败</button>
|
||||
<button class="filter-chip" data-status="done">已完成</button>
|
||||
</div>
|
||||
<div class="summary-stats-row">
|
||||
<span class="summary-stat">全部 <strong>{{ summary_total or 0 }}</strong></span>
|
||||
<span class="summary-stat summary-stat-pending">待总结 <strong>{{ summary_pending or 0 }}</strong></span>
|
||||
<span class="summary-stat summary-stat-failed">失败 <strong>{{ summary_failed or 0 }}</strong></span>
|
||||
<span class="summary-stat summary-stat-done">已完成 <strong>{{ summary_done or 0 }}</strong></span>
|
||||
</div>
|
||||
<div id="summary-list"
|
||||
hx-get="/admin/summary-status"
|
||||
hx-trigger="load"
|
||||
hx-target="#summary-list"
|
||||
hx-swap="innerHTML">
|
||||
<div class="empty-state"><p>加载中...</p></div>
|
||||
</div>
|
||||
<div class="summary-batch-actions">
|
||||
<button class="admin-action-btn" onclick="retryAllFailed()">🔄 重试所有失败</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理操作区 -->
|
||||
<div class="admin-actions">
|
||||
<h2 class="admin-actions-title">管理操作</h2>
|
||||
<div class="admin-action-buttons">
|
||||
<button class="admin-action-btn" onclick="adminAction('crawl')">
|
||||
🔄 抓取今天
|
||||
</button>
|
||||
<button class="admin-action-btn" onclick="adminAction('summarize')">
|
||||
📝 批量总结
|
||||
</button>
|
||||
<button class="admin-action-btn" onclick="adminAction('cleanup')">
|
||||
🧹 清理临时文件
|
||||
</button>
|
||||
<button class="admin-action-btn" onclick="adminAction('crawl')">🔄 抓取今天</button>
|
||||
<button class="admin-action-btn" onclick="adminAction('summarize')">📝 批量总结</button>
|
||||
<button class="admin-action-btn" onclick="adminAction('cleanup')">🧹 清理临时文件</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<style>
|
||||
/* ── Admin Logs ────────────────────────────────────────────────── */
|
||||
.admin-logs-page {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.admin-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 2px solid var(--border);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.admin-tab {
|
||||
padding: 10px 24px;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--ink-light);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
transition:
|
||||
color 0.2s,
|
||||
border-color 0.2s;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.admin-tab:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.admin-tab.active {
|
||||
color: var(--accent);
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
|
||||
.admin-tab-content {
|
||||
display: none;
|
||||
}
|
||||
.admin-tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ── Table ─────────────────────────────────────────────────────── */
|
||||
.admin-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.admin-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.85rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.admin-table th {
|
||||
padding: 10px 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--ink-light);
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-table td {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--ink);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.admin-table tbody tr:hover {
|
||||
background: var(--bg);
|
||||
}
|
||||
.admin-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.time-cell {
|
||||
white-space: nowrap;
|
||||
color: var(--ink-light);
|
||||
}
|
||||
.error-cell {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #c62828;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* ── Badges ────────────────────────────────────────────────────── */
|
||||
.task-badge,
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.task-crawl {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
}
|
||||
.task-summarize {
|
||||
background: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
}
|
||||
.task-cleanup {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
.task-delete {
|
||||
background: #fce4ec;
|
||||
color: #c62828;
|
||||
}
|
||||
.task-scheduler {
|
||||
background: #fff3e0;
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: #e8f5e9;
|
||||
color: #388e3c;
|
||||
}
|
||||
.status-running {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
.status-failed {
|
||||
background: #fce4ec;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
/* ── Admin Actions ─────────────────────────────────────────────── */
|
||||
.admin-actions {
|
||||
margin-top: 32px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.admin-actions-title {
|
||||
font-family: var(--font-body);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.admin-action-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.admin-action-btn {
|
||||
padding: 8px 18px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
.admin-action-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
box-shadow: 0 2px 8px var(--shadow);
|
||||
}
|
||||
|
||||
/* ── Responsive ────────────────────────────────────────────────── */
|
||||
@media (max-width: 640px) {
|
||||
.admin-table {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.admin-table th,
|
||||
.admin-table td {
|
||||
padding: 6px 8px;
|
||||
}
|
||||
.admin-action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
.admin-action-btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %} {% block scripts %}
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function adminAction(action) {
|
||||
const url = "/admin/" + action;
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
.then((r) => {
|
||||
if (r.status === 303 || r.status === 401) {
|
||||
window.location.href = "/admin/login";
|
||||
return;
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
alert(JSON.stringify(data, null, 2));
|
||||
location.reload();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
alert("请求失败: " + err.message);
|
||||
});
|
||||
function retrySummary(arxivId, btn) {
|
||||
btn.disabled=true; btn.textContent="处理中...";
|
||||
fetch("/admin/summarize/"+arxivId,{method:"POST",headers:{"Content-Type":"application/json"}})
|
||||
.then(r=>{if(r.status===303||r.status===401){window.location.href="/admin/login";return;}return r.json();})
|
||||
.then(data=>{if(data){showToast(data.error?"❌ "+data.error.substring(0,200):"✅ 已提交重试");setTimeout(()=>htmx.trigger("#summary-list","reloadSummary"),1000);}})
|
||||
.catch(err=>showToast("❌ 请求失败"))
|
||||
.finally(()=>{btn.disabled=false;btn.textContent="重试";});
|
||||
}
|
||||
function retryAllFailed() {
|
||||
if(!confirm("确定重试所有失败的总结任务?"))return;
|
||||
fetch("/admin/summary-retry-failed",{method:"POST",headers:{"Content-Type":"application/json"}})
|
||||
.then(r=>{if(r.status===303||r.status===401){window.location.href="/admin/login";return;}return r.json();})
|
||||
.then(data=>{if(data){showToast(data.error?"❌ "+data.error.substring(0,200):"✅ "+(data.message||"已提交"));setTimeout(()=>htmx.trigger("#summary-list","reloadSummary"),1500);}})
|
||||
.catch(err=>showToast("❌ 请求失败"));
|
||||
}
|
||||
|
||||
// Tab 切换
|
||||
document.querySelectorAll(".admin-tab").forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
document
|
||||
.querySelectorAll(".admin-tab")
|
||||
.forEach((t) => t.classList.remove("active"));
|
||||
document
|
||||
.querySelectorAll(".admin-tab-content")
|
||||
.forEach((c) => c.classList.remove("active"));
|
||||
document.querySelectorAll(".admin-tab").forEach(tab=>{
|
||||
tab.addEventListener("click",()=>{
|
||||
document.querySelectorAll(".admin-tab").forEach(t=>t.classList.remove("active"));
|
||||
document.querySelectorAll(".admin-tab-content").forEach(c=>c.classList.remove("active"));
|
||||
tab.classList.add("active");
|
||||
document.getElementById(tab.dataset.tab).classList.add("active");
|
||||
});
|
||||
});
|
||||
// 总结状态筛选
|
||||
document.querySelectorAll(".summary-filters .filter-chip").forEach(chip=>{
|
||||
chip.addEventListener("click",()=>{
|
||||
document.querySelectorAll(".summary-filters .filter-chip").forEach(c=>c.classList.remove("active"));
|
||||
chip.classList.add("active");
|
||||
htmx.ajax("GET","/admin/summary-status?status="+chip.dataset.status,"#summary-list");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
{% 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="date" name="date_from" value="{{ request.query_params.get('date_from', '') }}"
|
||||
class="paper-filter-input" title="起始日期" />
|
||||
<input type="date" name="date_to" value="{{ request.query_params.get('date_to', '') }}"
|
||||
class="paper-filter-input" title="结束日期" />
|
||||
<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>
|
||||
</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 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('%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 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-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 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('❌ 请求失败'));
|
||||
}
|
||||
}
|
||||
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>
|
||||
{% endblock %}
|
||||
@@ -6,7 +6,9 @@
|
||||
<title>{% block title %}HF Daily Papers{% endblock %}</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
{% if is_admin %}<link rel="stylesheet" href="/static/css/admin.css" />{% endif %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css" />
|
||||
{% block head_style %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
@@ -21,12 +23,12 @@
|
||||
/>
|
||||
</form>
|
||||
<div class="nav-links">
|
||||
<a href="/day/{{ today if today else '' }}">今日</a>
|
||||
<a id="nav-today-link" href="/">今日</a>
|
||||
<a href="/search">搜索</a>
|
||||
<a href="/trends">趋势</a>
|
||||
<a href="/reading-list">阅读列表</a>
|
||||
{% if is_admin %}
|
||||
<a href="/admin/logs">管理</a>
|
||||
<a href="/admin/">管理</a>
|
||||
<a href="/admin/logout" onclick="event.preventDefault();this.closest('form').submit()">退出</a>
|
||||
<form action="/admin/logout" method="post" style="display:none"></form>
|
||||
{% else %}
|
||||
|
||||
+29
-178
@@ -57,7 +57,7 @@ endblock %} {% block content %}
|
||||
<div class="quality-warning">📝 总结部分字段不完整</div>
|
||||
{% endif %} {% if paper.summary.one_line %}
|
||||
<section class="summary-section">
|
||||
<p class="one-line">{{ paper.summary.one_line }}</p>
|
||||
<p class="one-line">{{ paper.summary.one_line | safe }}</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
@@ -69,9 +69,9 @@ endblock %} {% block content %}
|
||||
{% for c in prereqs.concepts %}
|
||||
<div class="concept-card">
|
||||
<h3>{{ c.term }}</h3>
|
||||
<p>{{ c.explanation }}</p>
|
||||
<p>{{ c.explanation | safe }}</p>
|
||||
{% if c.why_matters %}
|
||||
<p class="concept-why">{{ c.why_matters }}</p>
|
||||
<p class="concept-why">{{ c.why_matters | safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -85,13 +85,13 @@ endblock %} {% block content %}
|
||||
<h2>研究动机</h2>
|
||||
<div class="motivation-block">
|
||||
{% if paper.summary.motivation_problem %}
|
||||
<p>{{ paper.summary.motivation_problem }}</p>
|
||||
<p>{{ paper.summary.motivation_problem | safe }}</p>
|
||||
{% endif %}
|
||||
{% if paper.summary.motivation_goal %}
|
||||
<p>本文的目标是{{ paper.summary.motivation_goal }}</p>
|
||||
<p>本文的目标是{{ paper.summary.motivation_goal | safe }}</p>
|
||||
{% endif %}
|
||||
{% if paper.summary.motivation_gap %}
|
||||
<p>与已有工作不同的是,{{ paper.summary.motivation_gap }}</p>
|
||||
<p>与已有工作不同的是,{{ paper.summary.motivation_gap | safe }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
@@ -102,21 +102,21 @@ endblock %} {% block content %}
|
||||
<section class="summary-section">
|
||||
<h2>核心方法</h2>
|
||||
{% if paper.summary.method_overview %}
|
||||
<p>{{ paper.summary.method_overview }}</p>
|
||||
<p>{{ paper.summary.method_overview | safe }}</p>
|
||||
{% endif %}
|
||||
<div class="key-idea">
|
||||
<p>{{ paper.summary.method_key_idea }}</p>
|
||||
<p>{{ paper.summary.method_key_idea | safe }}</p>
|
||||
</div>
|
||||
{% if paper.summary.method_steps_json %}
|
||||
<details>
|
||||
<summary>方法步骤详情</summary>
|
||||
<p>{{ paper.summary.method_steps_json }}</p>
|
||||
<p>{{ paper.summary.method_steps_json | safe }}</p>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% if paper.summary.method_novelty %}
|
||||
<details>
|
||||
<summary>技术新颖性</summary>
|
||||
<p>{{ paper.summary.method_novelty }}</p>
|
||||
<p>{{ paper.summary.method_novelty | safe }}</p>
|
||||
</details>
|
||||
{% endif %}
|
||||
</section>
|
||||
@@ -126,7 +126,7 @@ endblock %} {% block content %}
|
||||
{% if paper.summary.results_main_json %}
|
||||
<section class="summary-section">
|
||||
<h2>实验结果</h2>
|
||||
<p>{{ paper.summary.results_main_json }}</p>
|
||||
<p>{{ paper.summary.results_main_json | safe }}</p>
|
||||
{% if table_figures and table_figures|length > 0 %}
|
||||
{# 优先展示原文表格截图 #}
|
||||
{% for tf in table_figures %}
|
||||
@@ -189,24 +189,24 @@ endblock %} {% block content %}
|
||||
<section class="summary-section">
|
||||
<h2>局限与改进</h2>
|
||||
{% if paper.summary.limitations_json %}
|
||||
<p>{{ paper.summary.limitations_json }}</p>
|
||||
<p>{{ paper.summary.limitations_json | safe }}</p>
|
||||
{% endif %}
|
||||
{% if paper.summary.weaknesses_json %}
|
||||
<details>
|
||||
<summary>独立分析的弱点</summary>
|
||||
<p>{{ paper.summary.weaknesses_json }}</p>
|
||||
<p>{{ paper.summary.weaknesses_json | safe }}</p>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% if paper.summary.future_work_json %}
|
||||
<details>
|
||||
<summary>未来方向</summary>
|
||||
<p>{{ paper.summary.future_work_json }}</p>
|
||||
<p>{{ paper.summary.future_work_json | safe }}</p>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% if paper.summary.reproducibility %}
|
||||
<details>
|
||||
<summary>复现评估</summary>
|
||||
<p>{{ paper.summary.reproducibility }}</p>
|
||||
<p>{{ paper.summary.reproducibility | safe }}</p>
|
||||
</details>
|
||||
{% endif %}
|
||||
</section>
|
||||
@@ -290,9 +290,21 @@ endblock %} {% block content %}
|
||||
|
||||
{% block scripts %}
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js"
|
||||
onload="renderMathInElement(document.querySelector('.paper-detail'),{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}]});">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (typeof renderMathInElement === 'function') {
|
||||
renderMathInElement(document.querySelector('.paper-detail'), {
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
{ left: '$', right: '$', display: false }
|
||||
],
|
||||
throwOnError: false
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="/static/js/lightbox.js"></script>
|
||||
<style>
|
||||
.lightbox-overlay {
|
||||
position: fixed !important;
|
||||
@@ -356,165 +368,4 @@ endblock %} {% block content %}
|
||||
background: rgba(255,255,255,0.15);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function() {
|
||||
function openLightbox(src, alt) {
|
||||
var existing = document.querySelector('.lightbox-overlay');
|
||||
if (existing) existing.remove();
|
||||
|
||||
var overlay = document.createElement('div');
|
||||
overlay.className = 'lightbox-overlay';
|
||||
|
||||
var img = document.createElement('img');
|
||||
img.src = src;
|
||||
img.alt = alt || '';
|
||||
img.draggable = false;
|
||||
|
||||
// 工具栏
|
||||
var toolbar = document.createElement('div');
|
||||
toolbar.className = 'lightbox-toolbar';
|
||||
toolbar.innerHTML =
|
||||
'<button title="缩小">−</button>' +
|
||||
'<button title="放大">+</button>' +
|
||||
'<button title="适合窗口">⊡</button>' +
|
||||
'<button title="原始大小">1:1</button>' +
|
||||
'<button title="关闭">✕</button>';
|
||||
|
||||
overlay.appendChild(img);
|
||||
overlay.appendChild(toolbar);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// 视图状态
|
||||
var scale = 1, tx = 0, ty = 0;
|
||||
var baseW = 0, baseH = 0;
|
||||
var dragging = false, dragStartX = 0, dragStartY = 0, startTx = 0, startTy = 0;
|
||||
|
||||
function apply() {
|
||||
img.style.transform = 'translate(' + tx + 'px,' + ty + 'px) scale(' + scale + ')';
|
||||
}
|
||||
|
||||
function fitToScreen() {
|
||||
if (!baseW) return;
|
||||
var sw = window.innerWidth, sh = window.innerHeight;
|
||||
scale = Math.min(sw * 0.9 / baseW, sh * 0.9 / baseH, 1);
|
||||
tx = (sw - baseW * scale) / 2;
|
||||
ty = (sh - baseH * scale) / 2;
|
||||
apply();
|
||||
}
|
||||
|
||||
function resetOrigin() {
|
||||
scale = 1;
|
||||
tx = (window.innerWidth - baseW) / 2;
|
||||
ty = (window.innerHeight - baseH) / 2;
|
||||
apply();
|
||||
}
|
||||
|
||||
function zoomAt(factor, cx, cy) {
|
||||
var newScale = Math.max(0.1, Math.min(scale * factor, 20));
|
||||
// 保持鼠标指向的图片点不变
|
||||
tx = cx - (cx - tx) * (newScale / scale);
|
||||
ty = cy - (ty - ty) * (newScale / scale); // 这行有误,下面修正
|
||||
scale = newScale;
|
||||
apply();
|
||||
}
|
||||
|
||||
function zoomCenter(factor) {
|
||||
var cx = window.innerWidth / 2;
|
||||
var cy = window.innerHeight / 2;
|
||||
var newScale = Math.max(0.1, Math.min(scale * factor, 20));
|
||||
tx = cx - (cx - tx) * (newScale / scale);
|
||||
ty = cy - (cy - ty) * (newScale / scale);
|
||||
scale = newScale;
|
||||
apply();
|
||||
}
|
||||
|
||||
// 图片加载后初始化
|
||||
img.onload = function() {
|
||||
baseW = img.naturalWidth;
|
||||
baseH = img.naturalHeight;
|
||||
fitToScreen();
|
||||
};
|
||||
// 如果已缓存
|
||||
if (img.complete && img.naturalWidth) {
|
||||
baseW = img.naturalWidth;
|
||||
baseH = img.naturalHeight;
|
||||
fitToScreen();
|
||||
}
|
||||
|
||||
// 工具栏按钮
|
||||
var btns = toolbar.querySelectorAll('button');
|
||||
// 缩小 / 放大 / 适合 / 原始 / 关闭
|
||||
btns[0].onclick = function(e) { e.stopPropagation(); zoomCenter(0.7); };
|
||||
btns[1].onclick = function(e) { e.stopPropagation(); zoomCenter(1.4); };
|
||||
btns[2].onclick = function(e) { e.stopPropagation(); fitToScreen(); };
|
||||
btns[3].onclick = function(e) { e.stopPropagation(); resetOrigin(); };
|
||||
btns[4].onclick = function(e) { e.stopPropagation(); close(); };
|
||||
|
||||
// 滚轮缩放(以鼠标为中心)
|
||||
overlay.addEventListener('wheel', function(e) {
|
||||
e.preventDefault();
|
||||
var factor = e.deltaY < 0 ? 1.15 : 0.87;
|
||||
var rect = overlay.getBoundingClientRect();
|
||||
var cx = e.clientX - rect.left;
|
||||
var cy = e.clientY - rect.top;
|
||||
var newScale = Math.max(0.1, Math.min(scale * factor, 20));
|
||||
tx = cx - (cx - tx) * (newScale / scale);
|
||||
ty = cy - (cy - ty) * (newScale / scale);
|
||||
scale = newScale;
|
||||
apply();
|
||||
}, { passive: false });
|
||||
|
||||
// 拖拽平移
|
||||
overlay.addEventListener('pointerdown', function(e) {
|
||||
if (e.target.closest('.lightbox-toolbar')) return;
|
||||
dragging = true;
|
||||
dragStartX = e.clientX;
|
||||
dragStartY = e.clientY;
|
||||
startTx = tx;
|
||||
startTy = ty;
|
||||
img.classList.add('dragging');
|
||||
overlay.setPointerCapture(e.pointerId);
|
||||
});
|
||||
overlay.addEventListener('pointermove', function(e) {
|
||||
if (!dragging) return;
|
||||
tx = startTx + (e.clientX - dragStartX);
|
||||
ty = startTy + (e.clientY - dragStartY);
|
||||
apply();
|
||||
});
|
||||
overlay.addEventListener('pointerup', function() {
|
||||
dragging = false;
|
||||
img.classList.remove('dragging');
|
||||
});
|
||||
|
||||
// ESC 关闭
|
||||
function onKey(e) {
|
||||
if (e.key === 'Escape') { close(); }
|
||||
else if (e.key === '+' || e.key === '=') { zoomCenter(1.4); }
|
||||
else if (e.key === '-') { zoomCenter(0.7); }
|
||||
else if (e.key === '0') { fitToScreen(); }
|
||||
}
|
||||
|
||||
function close() {
|
||||
overlay.remove();
|
||||
document.removeEventListener('keydown', onKey);
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', onKey);
|
||||
|
||||
// 激活动画
|
||||
requestAnimationFrame(function() {
|
||||
overlay.classList.add('active');
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
var img = e.target;
|
||||
if (img.tagName !== 'IMG') return;
|
||||
if (!img.closest('.inline-figure') && !img.closest('.gallery-item')) return;
|
||||
if (img.closest('.lightbox-overlay')) return;
|
||||
e.preventDefault();
|
||||
openLightbox(img.src, img.alt);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %} {% block title %}{{ page_title }} — HF Daily Papers{%
|
||||
{% extends "base.html" %}{% from "partials/paper_card.html" import render_card %}
|
||||
{% block title %}{{ page_title }} — HF Daily Papers{%
|
||||
endblock %} {% block content %}
|
||||
<div class="date-nav">
|
||||
{% if prev_day %}
|
||||
@@ -8,13 +9,12 @@ endblock %} {% block content %}
|
||||
{% if next_day <= today %}
|
||||
<a href="/day/{{ next_day }}" class="date-nav-btn">后一天 →</a>
|
||||
{% endif %}
|
||||
<a href="/day/{{ today }}" class="date-nav-btn">今日</a>
|
||||
<a href="/" class="date-nav-btn">今日</a>
|
||||
</div>
|
||||
|
||||
{% if papers %}
|
||||
<div class="paper-list">
|
||||
{% for paper in papers %} {% include "partials/paper_card.html" %} {% endfor
|
||||
%}
|
||||
{% for paper in papers %}{{ render_card(paper) }}{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
|
||||
@@ -40,111 +40,4 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.login-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 60vh;
|
||||
padding: 40px 16px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 36px 32px;
|
||||
box-shadow: 0 4px 24px var(--shadow);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-family: var(--font-body);
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 0.9rem;
|
||||
color: var(--ink-light);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
background: #fce4ec;
|
||||
color: #c62828;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.login-field label {
|
||||
display: block;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.login-field input {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.9rem;
|
||||
font-family: var(--font-sans);
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
transition: border-color 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.login-field input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px rgba(27, 54, 93, 0.1);
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
font-family: var(--font-sans);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-card {
|
||||
padding: 28px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %} {% block title %}{{ page_title }} — HF Daily Papers{%
|
||||
{% extends "base.html" %}{% from "partials/paper_card.html" import render_card %}
|
||||
{% block title %}{{ page_title }} — HF Daily Papers{%
|
||||
endblock %} {% block content %}
|
||||
<section class="reading-list-page">
|
||||
<h1 class="page-heading">📖 阅读列表</h1>
|
||||
@@ -55,8 +56,7 @@ endblock %} {% block content %}
|
||||
</div>
|
||||
{% endif %} {% if papers %}
|
||||
<div class="paper-list">
|
||||
{% for paper in papers %} {% include "partials/paper_card.html" %} {% endfor
|
||||
%}
|
||||
{% for paper in papers %}{{ render_card(paper) }}{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %} {% block title %}{{ page_title }} — HF Daily Papers{%
|
||||
{% extends "base.html" %}{% from "partials/paper_card.html" import render_card %}
|
||||
{% block title %}{{ page_title }} — HF Daily Papers{%
|
||||
endblock %} {% block content %}
|
||||
<section class="search-page">
|
||||
{# 搜索表单 #}
|
||||
@@ -81,67 +82,7 @@ endblock %} {% block content %}
|
||||
{% if results %}
|
||||
<div class="paper-list">
|
||||
{% for paper in results %}
|
||||
<article class="paper-card search-result" data-arxiv="{{ paper.arxiv_id }}">
|
||||
<div class="paper-card-header">
|
||||
<h2 class="paper-title">
|
||||
<a href="/paper/{{ paper.arxiv_id }}">
|
||||
{% set snippet = snippets.get(paper.id, {}) %} {% if snippet and
|
||||
snippet.title_zh %} {{ snippet.title_zh | safe }} {% elif
|
||||
paper.title_zh %} {{ paper.title_zh }} {% else %} {{ paper.title_en
|
||||
}} {% endif %}
|
||||
</a>
|
||||
</h2>
|
||||
<span class="paper-upvotes">👍 {{ paper.upvotes }}</span>
|
||||
{% if distances and paper.arxiv_id in distances %}
|
||||
<span class="similarity-score" title="语义相似度距离">
|
||||
🎯 {{ "%.3f"|format(distances[paper.arxiv_id]) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if snippet and snippet.abstract %}
|
||||
<p class="paper-snippet">{{ snippet.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 %}
|
||||
|
||||
<div class="paper-meta">
|
||||
<span class="paper-authors">
|
||||
{{ paper.authors|map(attribute='name')|join(', ')|truncate(80) }}
|
||||
</span>
|
||||
<span class="paper-date">{{ paper.paper_date }}</span>
|
||||
</div>
|
||||
|
||||
<div class="paper-tags">
|
||||
{% for t in paper.tags[:5] %}
|
||||
<span class="tag">{{ t.tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="paper-footer">
|
||||
<span
|
||||
class="summary-badge summary-{{ paper.summary_status.status if paper.summary_status else 'none' }}"
|
||||
>
|
||||
{# djlint:off #}
|
||||
{% if not paper.summary_status or paper.summary_status.status == 'pending' %}
|
||||
未总结
|
||||
{% elif paper.summary_status.status == 'processing' %}
|
||||
🔄 总结中
|
||||
{% elif paper.summary_status.status in ('failed', 'permanent_failure') %}
|
||||
❌ 总结失败
|
||||
{% elif paper.summary_status.status == 'done' %}
|
||||
✅ 已总结
|
||||
{% endif %}
|
||||
{# djlint:on #}
|
||||
</span>
|
||||
<a href="/paper/{{ paper.arxiv_id }}" class="btn-detail">详情 →</a>
|
||||
</div>
|
||||
</article>
|
||||
{{ render_card(paper, snippets=snippets, distances=distances, variant="search") }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user