feat: overhaul UI styling, improve templates, enhance services and tests

This commit is contained in:
2026-06-06 00:38:56 +08:00
parent f7f1a4c0cb
commit 904eec392e
38 changed files with 1471 additions and 795 deletions
+161 -77
View File
@@ -1,8 +1,5 @@
{% extends "base.html" %}
{% block title %}管理日志 — HF Daily Papers{% endblock %}
{% block content %}
{% extends "base.html" %} {% block title %}管理日志 — HF Daily Papers{% endblock
%} {% block content %}
<div class="admin-logs-page">
<h1 class="page-heading">📋 管理日志</h1>
@@ -34,21 +31,31 @@
{% 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="task-badge task-{{ log.task }}">{{ log.task }}</span>
</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 %}
{% if log.status == 'success' %}✓ 成功 {% elif log.status ==
'running' %}⟳ 运行中 {% elif log.status == 'failed' %}✗ 失败 {%
else %}{{ log.status }}{% endif %}
</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[:80] + '...' if log.error and log.error|length > 80 else (log.error or '-') }}</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 '-') }}
</td>
</tr>
{% endfor %}
</tbody>
@@ -90,15 +97,23 @@
<td>{{ job.paper_count or 0 }}</td>
<td>
<span class="status-badge status-{{ job.status }}">
{% if job.status == 'success' %}✓ 成功
{% elif job.status == 'running' %}⟳ 运行中
{% elif job.status == 'failed' %}✗ 失败
{% else %}{{ job.status }}{% endif %}
{% if job.status == 'success' %}✓ 成功 {% elif job.status ==
'running' %}⟳ 运行中 {% elif job.status == 'failed' %}✗ 失败 {%
else %}{{ job.status }}{% endif %}
</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 '-') }}</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 '-') }}
</td>
</tr>
{% endfor %}
</tbody>
@@ -116,16 +131,24 @@
<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>
<style>
/* ── Admin Logs ────────────────────────────────────────────────── */
.admin-logs-page { max-width: 100%; }
.admin-logs-page {
max-width: 100%;
}
.admin-tabs {
display: flex;
@@ -144,22 +167,32 @@
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: color 0.2s, border-color 0.2s;
transition:
color 0.2s,
border-color 0.2s;
font-family: var(--font-sans);
}
.admin-tab:hover { color: var(--accent); }
.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; }
.admin-tab-content {
display: none;
}
.admin-tab-content.active {
display: block;
}
/* ── Table ─────────────────────────────────────────────────────── */
.admin-table-wrap { overflow-x: auto; }
.admin-table-wrap {
overflow-x: auto;
}
.admin-table {
width: 100%;
@@ -187,14 +220,29 @@
vertical-align: middle;
}
.admin-table tbody tr:hover { background: var(--bg); }
.admin-table tbody tr:last-child td { border-bottom: none; }
.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; }
.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 {
.task-badge,
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
@@ -202,15 +250,39 @@
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; }
.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; }
.status-success {
background: #e8f5e9;
color: #388e3c;
}
.status-running {
background: #e3f2fd;
color: #1976d2;
}
.status-failed {
background: #fce4ec;
color: #c62828;
}
/* ── Admin Actions ─────────────────────────────────────────────── */
.admin-actions {
@@ -254,46 +326,58 @@
/* ── 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; }
.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 %}
{% endblock %} {% block scripts %}
<script>
function adminAction(action) {
const token = prompt('请输入 Admin Token:');
if (!token) return;
function adminAction(action) {
const token = prompt("请输入 Admin Token:");
if (!token) return;
const url = '/admin/' + action;
fetch(url, {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
},
})
.then(r => r.json())
.then(data => {
alert(JSON.stringify(data, null, 2));
location.reload();
})
.catch(err => {
alert('请求失败: ' + err.message);
});
}
const url = "/admin/" + action;
fetch(url, {
method: "POST",
headers: {
Authorization: "Bearer " + token,
"Content-Type": "application/json",
},
})
.then((r) => r.json())
.then((data) => {
alert(JSON.stringify(data, null, 2));
location.reload();
})
.catch((err) => {
alert("请求失败: " + err.message);
});
}
// 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'));
tab.classList.add('active');
document.getElementById(tab.dataset.tab).classList.add('active');
// 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"));
tab.classList.add("active");
document.getElementById(tab.dataset.tab).classList.add("active");
});
});
});
</script>
{% endblock %}