chore: reorganize project into monorepo
This commit is contained in:
682
backend/assets/views/admin/base.html
Normal file
682
backend/assets/views/admin/base.html
Normal file
@@ -0,0 +1,682 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ page_title | default(value="Termi Admin") }} · Termi Admin</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f4f4f5;
|
||||
--bg-panel: rgba(255, 255, 255, 0.88);
|
||||
--bg-panel-strong: rgba(255, 255, 255, 0.98);
|
||||
--line: rgba(24, 24, 27, 0.09);
|
||||
--line-strong: rgba(24, 24, 27, 0.16);
|
||||
--text: #09090b;
|
||||
--text-soft: #52525b;
|
||||
--text-mute: #71717a;
|
||||
--accent: #18181b;
|
||||
--accent-2: #2563eb;
|
||||
--accent-3: #dc2626;
|
||||
--accent-4: #16a34a;
|
||||
--shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
|
||||
--radius-xl: 24px;
|
||||
--radius-lg: 18px;
|
||||
--radius-md: 12px;
|
||||
--font-sans: "Inter", "Segoe UI", "PingFang SC", sans-serif;
|
||||
--font-mono: "JetBrains Mono", "Cascadia Code", monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: var(--font-sans);
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(37, 99, 235, 0.08), transparent 30%),
|
||||
linear-gradient(180deg, #fafafa 0%, #f4f4f5 100%);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-columns: 290px minmax(0, 1fr);
|
||||
min-height: 100vh;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.sidebar,
|
||||
.surface,
|
||||
.stat,
|
||||
.table-panel,
|
||||
.hero-card,
|
||||
.form-panel,
|
||||
.login-panel {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-xl);
|
||||
background: var(--bg-panel);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
padding: 28px 22px;
|
||||
position: sticky;
|
||||
top: 24px;
|
||||
height: calc(100vh - 48px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 16px;
|
||||
background: #111827;
|
||||
border: 1px solid #111827;
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
margin: 14px 0 6px;
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
.brand-copy {
|
||||
margin: 0;
|
||||
color: var(--text-soft);
|
||||
line-height: 1.6;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.nav-group {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 18px;
|
||||
color: var(--text-soft);
|
||||
border: 1px solid transparent;
|
||||
transition: 160ms ease;
|
||||
}
|
||||
|
||||
.nav-item:hover,
|
||||
.nav-item.active {
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
border-color: var(--line);
|
||||
color: var(--text);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
box-shadow: inset 0 0 0 1px rgba(24, 24, 27, 0.06);
|
||||
}
|
||||
|
||||
.nav-kicker {
|
||||
margin-top: auto;
|
||||
padding: 18px;
|
||||
border-radius: 22px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.nav-kicker strong {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.nav-kicker p {
|
||||
margin: 0;
|
||||
color: var(--text-soft);
|
||||
line-height: 1.55;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.content-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.surface {
|
||||
padding: 26px 28px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(24, 24, 27, 0.05);
|
||||
color: var(--text-soft);
|
||||
font-size: 0.84rem;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 12px 0 8px;
|
||||
font-size: clamp(1.7rem, 2.2vw, 2.5rem);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
margin: 0;
|
||||
max-width: 760px;
|
||||
color: var(--text-soft);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 44px;
|
||||
padding: 0 16px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: 160ms ease;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: #fafafa;
|
||||
box-shadow: 0 10px 24px rgba(24, 24, 27, 0.16);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
border-color: var(--line);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: rgba(220, 38, 38, 0.08);
|
||||
border-color: rgba(220, 38, 38, 0.14);
|
||||
color: var(--accent-3);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: rgba(22, 163, 74, 0.08);
|
||||
border-color: rgba(22, 163, 74, 0.14);
|
||||
color: var(--accent-4);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border-color: rgba(245, 158, 11, 0.16);
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.stats-grid,
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat,
|
||||
.hero-card,
|
||||
.table-panel,
|
||||
.form-panel {
|
||||
padding: 22px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--bg-panel-strong);
|
||||
}
|
||||
|
||||
.stat-label,
|
||||
.muted,
|
||||
.table-note,
|
||||
.field-hint,
|
||||
.badge-soft {
|
||||
color: var(--text-mute);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
margin: 10px 0 6px;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tone-blue .stat-value { color: var(--accent-2); }
|
||||
.tone-gold .stat-value { color: var(--accent); }
|
||||
.tone-green .stat-value { color: var(--accent-4); }
|
||||
.tone-pink .stat-value { color: var(--accent-3); }
|
||||
.tone-violet .stat-value { color: #7a5ef4; }
|
||||
|
||||
.table-panel {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.table-head h2,
|
||||
.hero-card h2 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
overflow: auto;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 880px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 16px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
border-bottom: 1px solid rgba(93, 76, 56, 0.1);
|
||||
}
|
||||
|
||||
th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: rgba(250, 250, 250, 0.98);
|
||||
color: var(--text-soft);
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.item-title strong {
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.item-meta,
|
||||
.mono {
|
||||
color: var(--text-soft);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.84rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.badge,
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.78rem;
|
||||
border: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
color: var(--accent-4);
|
||||
background: rgba(93, 122, 45, 0.1);
|
||||
border-color: rgba(93, 122, 45, 0.14);
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
color: var(--accent);
|
||||
background: rgba(202, 94, 45, 0.1);
|
||||
border-color: rgba(202, 94, 45, 0.14);
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
color: var(--accent-3);
|
||||
background: rgba(156, 61, 84, 0.1);
|
||||
border-color: rgba(156, 61, 84, 0.14);
|
||||
}
|
||||
|
||||
.chip {
|
||||
background: rgba(241, 245, 249, 0.95);
|
||||
color: var(--text-soft);
|
||||
border-color: rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.inline-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.inline-link {
|
||||
color: var(--accent-2);
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.inline-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 40px 18px;
|
||||
text-align: center;
|
||||
color: var(--text-soft);
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.field-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.field label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-soft);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.field input,
|
||||
.field textarea {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
color: var(--text);
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.field textarea {
|
||||
resize: vertical;
|
||||
min-height: 132px;
|
||||
}
|
||||
|
||||
.inline-form {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.inline-form.compact {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.compact-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 8px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.compact-grid textarea,
|
||||
.compact-grid input,
|
||||
.compact-grid select {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
color: var(--text);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.compact-grid textarea {
|
||||
min-height: 84px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.compact-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.notice {
|
||||
display: none;
|
||||
margin-top: 14px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.notice.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.notice-success {
|
||||
color: var(--accent-4);
|
||||
background: rgba(22, 163, 74, 0.08);
|
||||
border-color: rgba(22, 163, 74, 0.14);
|
||||
}
|
||||
|
||||
.notice-error {
|
||||
color: var(--accent-3);
|
||||
background: rgba(220, 38, 38, 0.08);
|
||||
border-color: rgba(220, 38, 38, 0.14);
|
||||
}
|
||||
|
||||
.login-shell {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
width: min(520px, 100%);
|
||||
padding: 34px;
|
||||
}
|
||||
|
||||
.login-panel h1 {
|
||||
margin: 18px 0 10px;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.login-panel p {
|
||||
margin: 0;
|
||||
color: var(--text-soft);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
display: none;
|
||||
margin-top: 18px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
background: rgba(220, 38, 38, 0.08);
|
||||
border: 1px solid rgba(220, 38, 38, 0.14);
|
||||
color: var(--accent-3);
|
||||
}
|
||||
|
||||
.login-error.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: static;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.shell,
|
||||
.surface {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
table {
|
||||
min-width: 760px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}
|
||||
<div class="shell">
|
||||
<aside class="sidebar">
|
||||
<div>
|
||||
<div class="brand-mark">/></div>
|
||||
<h1 class="brand-title">Termi Admin</h1>
|
||||
<p class="brand-copy">后台数据直接联动前台页面。你可以在这里审核评论和友链、检查分类标签,并跳到对应前台页面确认效果。</p>
|
||||
</div>
|
||||
|
||||
<nav class="nav-group">
|
||||
<a href="/admin" class="nav-item {% if active_nav == 'dashboard' %}active{% endif %}">概览面板</a>
|
||||
<a href="/admin/posts" class="nav-item {% if active_nav == 'posts' %}active{% endif %}">文章管理</a>
|
||||
<a href="/admin/comments" class="nav-item {% if active_nav == 'comments' %}active{% endif %}">评论审核</a>
|
||||
<a href="/admin/categories" class="nav-item {% if active_nav == 'categories' %}active{% endif %}">分类管理</a>
|
||||
<a href="/admin/tags" class="nav-item {% if active_nav == 'tags' %}active{% endif %}">标签管理</a>
|
||||
<a href="/admin/reviews" class="nav-item {% if active_nav == 'reviews' %}active{% endif %}">评价管理</a>
|
||||
<a href="/admin/friend_links" class="nav-item {% if active_nav == 'friend_links' %}active{% endif %}">友链申请</a>
|
||||
<a href="/admin/site-settings" class="nav-item {% if active_nav == 'site_settings' %}active{% endif %}">站点设置</a>
|
||||
</nav>
|
||||
|
||||
<div class="nav-kicker">
|
||||
<strong>前台联调入口</strong>
|
||||
<p>所有管理页都带了前台直达链接,处理完数据后可以立刻跳转验证。</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="content-shell">
|
||||
<header class="surface topbar">
|
||||
<div>
|
||||
<span class="eyebrow">Unified Admin</span>
|
||||
<h1 class="page-title">{{ page_title | default(value="Termi Admin") }}</h1>
|
||||
<p class="page-description">{{ page_description | default(value="统一处理后台数据与前台联调。") }}</p>
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
{% for item in header_actions | default(value=[]) %}
|
||||
<a
|
||||
href="{{ item.href }}"
|
||||
class="btn btn-{{ item.variant }}"
|
||||
{% if item.external %}target="_blank" rel="noreferrer noopener"{% endif %}
|
||||
>
|
||||
{{ item.label }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
<a href="/admin/logout" class="btn btn-danger">退出后台</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="content-grid">
|
||||
{% block main_content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<script>
|
||||
async function adminPatch(url, payload, successMessage) {
|
||||
const response = await fetch(url, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text() || "request failed");
|
||||
}
|
||||
|
||||
if (successMessage) {
|
||||
alert(successMessage);
|
||||
}
|
||||
|
||||
location.reload();
|
||||
}
|
||||
</script>
|
||||
{% block page_scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user