feat: 重构后台布局并添加站点动态设置功能

This commit is contained in:
seahi committed 2026-07-29 10:49:01 +08:00
1 parent 69d6129ea8
commit 068b3dbbbb
6 files changed
+410 -30

No files matched your search

+44 -2
View File
@@ -1,7 +1,49 @@
import type { LayoutServerLoad } from './$types'; import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => { export const load: LayoutServerLoad = async ({ locals, platform }) => {
const db = platform?.env?.DB;
let aliases = {
post: '文章',
page: '页面',
category: '分类',
tag: '标签'
};
let site = {
title: 'BlogFlare',
description: 'A high-performance blog powered by Cloudflare D1, R2, and KV.'
};
if (db) {
try {
const { results } = await db.prepare(
"SELECT key, value FROM settings WHERE key LIKE 'alias.%' OR key LIKE 'site.%'"
).all();
if (results && results.length > 0) {
results.forEach((row: any) => {
if (row.key.startsWith('alias.')) {
const type = row.key.split('.')[1];
if (type && type in aliases) {
(aliases as any)[type] = row.value;
}
} else if (row.key.startsWith('site.')) {
const type = row.key.split('.')[1];
if (type && type in site) {
(site as any)[type] = row.value;
}
}
});
}
} catch (e) {
console.error('Failed to load settings', e);
}
}
return { return {
user: locals.user user: locals.user,
aliases,
site
}; };
}; };
+17 -7
View File
@@ -17,24 +17,34 @@
</script> </script>
<svelte:head> <svelte:head>
<title>后台管理 - BlogFlare</title> <title>后台管理 - {$page.data.site?.title || 'BlogFlare'}</title>
</svelte:head> </svelte:head>
<header class="header {scrolled ? 'scrolled' : ''}"> <header class="header {scrolled ? 'scrolled' : ''}">
<div class="container wide"> <div class="container wide">
<a href="/admin" class="brand">BlogFlare Admin</a> <a href="/admin" class="brand">{$page.data.site?.title || 'BlogFlare'} Admin</a>
<nav> <nav>
<a href="/admin" class={$page.url.pathname === '/admin' ? 'active' : ''}>仪表盘</a> <a href="/admin" class={$page.url.pathname === '/admin' ? 'active' : ''}>仪表盘</a>
<a href="/admin/posts/new" class={$page.url.pathname === '/admin/posts/new' ? 'active' : ''}>撰写</a> <div class="nav-dropdown">
<span class="nav-dropdown-trigger {$page.url.pathname === '/admin/posts/new' || $page.url.pathname === '/admin/pages/new' ? 'active' : ''}">
撰写
</span>
<div class="dropdown-menu">
<a href="/admin/posts/new">撰写{$page.data.aliases?.post || '文章'}</a>
<a href="/admin/pages/new">创建{$page.data.aliases?.page || '页面'}</a>
</div>
</div>
<div class="nav-dropdown"> <div class="nav-dropdown">
<span class="nav-dropdown-trigger {($page.url.pathname.startsWith('/admin/posts') && $page.url.pathname !== '/admin/posts/new') || $page.url.pathname.startsWith('/admin/categories') || $page.url.pathname.startsWith('/admin/tags') ? 'active' : ''}"> <span class="nav-dropdown-trigger {($page.url.pathname.startsWith('/admin/posts') && $page.url.pathname !== '/admin/posts/new') || $page.url.pathname.startsWith('/admin/pages') && $page.url.pathname !== '/admin/pages/new' || $page.url.pathname.startsWith('/admin/categories') || $page.url.pathname.startsWith('/admin/tags') || $page.url.pathname.startsWith('/admin/files') ? 'active' : ''}">
管理 管理
</span> </span>
<div class="dropdown-menu"> <div class="dropdown-menu">
<a href="/admin/posts">文章</a> <a href="/admin/posts">{$page.data.aliases?.post || '文章'}</a>
<a href="/admin/categories">分类</a> <a href="/admin/pages">{$page.data.aliases?.page || '页面'}</a>
<a href="/admin/tags">标签</a> <a href="/admin/categories">{$page.data.aliases?.category || '分类'}</a>
<a href="/admin/tags">{$page.data.aliases?.tag || '标签'}</a>
<a href="/admin/files">文件</a>
</div> </div>
</div> </div>
+9 -1
View File
@@ -5,14 +5,20 @@ export const load: PageServerLoad = async ({ platform }) => {
let postCount = 0; let postCount = 0;
let tagCount = 0; let tagCount = 0;
let categoryCount = 0;
let pageCount = 0;
if (db) { if (db) {
try { try {
const postResult = await db.prepare('SELECT COUNT(*) as count FROM posts').first(); const postResult = await db.prepare('SELECT COUNT(*) as count FROM posts').first();
const tagResult = await db.prepare('SELECT COUNT(*) as count FROM tags').first(); const tagResult = await db.prepare('SELECT COUNT(*) as count FROM tags').first();
const categoryResult = await db.prepare('SELECT COUNT(*) as count FROM categories').first();
const pageResult = await db.prepare('SELECT COUNT(*) as count FROM pages').first();
postCount = (postResult?.count as number) || 0; postCount = (postResult?.count as number) || 0;
tagCount = (tagResult?.count as number) || 0; tagCount = (tagResult?.count as number) || 0;
categoryCount = (categoryResult?.count as number) || 0;
pageCount = (pageResult?.count as number) || 0;
} catch (err) { } catch (err) {
console.error('Error fetching admin dashboard stats:', err); console.error('Error fetching admin dashboard stats:', err);
} }
@@ -20,6 +26,8 @@ export const load: PageServerLoad = async ({ platform }) => {
return { return {
postCount, postCount,
tagCount tagCount,
categoryCount,
pageCount
}; };
}; };
+40 -8
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
import { page } from '$app/stores';
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
</script> </script>
@@ -9,15 +10,35 @@
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-card"> <div class="stat-card">
<h3>文章总数</h3> <h3>{$page.data.aliases?.post || '文章'}总数</h3>
<div class="number">{data.postCount}</div> <div class="number">{data.postCount}</div>
<a href="/admin/posts" class="action-link">管理文章 &rarr;</a> <a href="/admin/posts" class="action-link">
管理{$page.data.aliases?.post || '文章'} <svg class="arrow-icon" xmlns="http://www.w3.org/2000/svg" width="1.3em" height="1.3em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="7" y1="17" x2="17" y2="7"></line><polyline points="7 7 17 7 17 17"></polyline></svg>
</a>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h3>标签总数</h3> <h3>{$page.data.aliases?.page || '页面'}总数</h3>
<div class="number">{data.pageCount}</div>
<a href="/admin/pages" class="action-link">
管理{$page.data.aliases?.page || '页面'} <svg class="arrow-icon" xmlns="http://www.w3.org/2000/svg" width="1.3em" height="1.3em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="7" y1="17" x2="17" y2="7"></line><polyline points="7 7 17 7 17 17"></polyline></svg>
</a>
</div>
<div class="stat-card">
<h3>{$page.data.aliases?.category || '分类'}总数</h3>
<div class="number">{data.categoryCount}</div>
<a href="/admin/categories" class="action-link">
管理{$page.data.aliases?.category || '分类'} <svg class="arrow-icon" xmlns="http://www.w3.org/2000/svg" width="1.3em" height="1.3em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="7" y1="17" x2="17" y2="7"></line><polyline points="7 7 17 7 17 17"></polyline></svg>
</a>
</div>
<div class="stat-card">
<h3>{$page.data.aliases?.tag || '标签'}总数</h3>
<div class="number">{data.tagCount}</div> <div class="number">{data.tagCount}</div>
<a href="/admin/tags" class="action-link">管理标签 &rarr;</a> <a href="/admin/tags" class="action-link">
管理{$page.data.aliases?.tag || '标签'} <svg class="arrow-icon" xmlns="http://www.w3.org/2000/svg" width="1.3em" height="1.3em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="7" y1="17" x2="17" y2="7"></line><polyline points="7 7 17 7 17 17"></polyline></svg>
</a>
</div> </div>
</div> </div>
</div> </div>
@@ -52,7 +73,7 @@
} }
.stat-card:hover { .stat-card:hover {
border-color: var(--accent-color, #bd6262); border-color: var(--accent-color, #419b38);
} }
.stat-card h3 { .stat-card h3 {
@@ -71,16 +92,27 @@
} }
.action-link { .action-link {
display: inline-block; display: inline-flex;
align-items: center;
font-size: 0.95rem; font-size: 0.95rem;
color: var(--text-color); color: var(--text-color);
font-weight: 600; font-weight: 600;
border-bottom: 1px solid rgba(0, 0, 0, 0.2); border-bottom: 1px solid rgba(0, 0, 0, 0.2);
transition: all 0.2s; transition: all 0.2s;
text-decoration: none;
}
.arrow-icon {
margin-left: 6px;
transition: transform 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.stat-card:hover .arrow-icon {
transform: translate(3px, -3px);
} }
.stat-card:hover .action-link { .stat-card:hover .action-link {
color: var(--accent-color, #bd6262); color: var(--accent-color, #419b38);
border-bottom-color: var(--accent-color, #bd6262); border-bottom-color: var(--accent-color, #419b38);
} }
</style> </style>
+73 -3
View File
@@ -1,8 +1,78 @@
import type { PageServerLoad } from './$types'; import { fail } from '@sveltejs/kit';
import type { PageServerLoad, Actions } from './$types';
export const load: PageServerLoad = async ({ locals, platform }) => {
const db = platform?.env?.DB;
let settings: Record<string, string> = {};
if (db) {
try {
const { results } = await db.prepare("SELECT key, value FROM settings").all();
results.forEach((row: any) => {
settings[row.key] = row.value;
});
} catch (e) {
console.error('Failed to load settings', e);
}
}
export const load: PageServerLoad = async ({ platform, locals }) => {
return { return {
user: locals.user, user: locals.user,
platformEnv: !!platform?.env?.DB ? 'Cloudflare D1 (Connected)' : 'Local Mock' platformEnv: !!db ? 'Cloudflare D1 (Connected)' : 'Local Mock',
settings
}; };
}; };
export const actions: Actions = {
updateSiteInfo: async ({ request, platform }) => {
const db = platform?.env?.DB;
if (!db) return fail(500, { error: '数据库未连接' });
const data = await request.formData();
const updates = [
{ key: 'site.title', value: data.get('site_title')?.toString() || 'BlogFlare' },
{ key: 'site.description', value: data.get('site_description')?.toString() || 'A high-performance blog powered by Cloudflare D1, R2, and KV.' },
{ key: 'r2.public_domain', value: data.get('r2_public_domain')?.toString() || 'https://assets.yourdomain.com' }
];
try {
for (const { key, value } of updates) {
await db.prepare(
`INSERT INTO settings (key, value, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP`
).bind(key, value).run();
}
return { success: true };
} catch (e) {
console.error('Failed to update site info', e);
return fail(500, { error: '保存站点信息失败' });
}
},
updateAliases: async ({ request, platform }) => {
const db = platform?.env?.DB;
if (!db) return fail(500, { error: '数据库未连接' });
const data = await request.formData();
const updates = [
{ key: 'alias.post', value: data.get('alias_post')?.toString() || '文章' },
{ key: 'alias.page', value: data.get('alias_page')?.toString() || '页面' },
{ key: 'alias.category', value: data.get('alias_category')?.toString() || '分类' },
{ key: 'alias.tag', value: data.get('alias_tag')?.toString() || '标签' }
];
try {
for (const { key, value } of updates) {
await db.prepare(
`INSERT INTO settings (key, value, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP`
).bind(key, value).run();
}
return { success: true };
} catch (e) {
console.error('Failed to update aliases', e);
return fail(500, { error: '保存失败' });
}
}
};
+227 -9
View File
@@ -1,20 +1,66 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData, ActionData } from './$types';
import { enhance } from '$app/forms';
let { data }: { data: PageData } = $props(); let { data, form }: { data: PageData; form: ActionData } = $props();
let activeTab = $state('basic');
</script> </script>
<div class="admin-page-header"> <div class="admin-page-header">
<h2>系统设置</h2> <h2>系统设置</h2>
</div> </div>
<div class="settings-container"> <div class="tabs-header">
<div class="settings-card"> <button class="tab-btn {activeTab === 'basic' ? 'active' : ''}" onclick={() => activeTab = 'basic'}>基础信息</button>
<h3>博客信息</h3> <button class="tab-btn {activeTab === 'terms' ? 'active' : ''}" onclick={() => activeTab = 'terms'}>术语设定</button>
<div class="info-row"> <button class="tab-btn {activeTab === 'system' ? 'active' : ''}" onclick={() => activeTab = 'system'}>系统与安全</button>
<span class="label">博客名称</span>
<span class="value">BlogFlare</span>
</div> </div>
{#if form?.success}
<div class="success-alert">
设置保存成功!
</div>
{/if}
{#if form?.error}
<div class="error-alert">
{form.error}
</div>
{/if}
<div class="settings-container">
{#if activeTab === 'basic'}
<div class="settings-card full-width">
<h3>站点基础信息</h3>
<p class="desc">设置您的网站名称和一句话描述,这些信息将展示在首页和浏览器标题中。</p>
<form method="POST" action="?/updateSiteInfo" use:enhance class="alias-form">
<div class="form-row">
<div class="form-group">
<label for="site_title">网站标题</label>
<input type="text" id="site_title" name="site_title" value={data.settings['site.title'] || 'BlogFlare'} required>
</div>
<div class="form-group">
<label for="site_description">站点描述</label>
<input type="text" id="site_description" name="site_description" value={data.settings['site.description'] || 'A high-performance blog powered by Cloudflare D1, R2, and KV.'} required>
</div>
<div class="form-group">
<label for="r2_public_domain">R2 自定义域名 (用于访问附件)</label>
<input type="text" id="r2_public_domain" name="r2_public_domain" value={data.settings['r2.public_domain'] || 'https://assets.yourdomain.com'} placeholder="例如: https://assets.mydomain.com">
<small class="form-hint">请确保不要以斜杠 / 结尾</small>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
{/if}
{#if activeTab === 'system'}
<div class="settings-card">
<h3>系统信息</h3>
<div class="info-row"> <div class="info-row">
<span class="label">主题风格</span> <span class="label">主题风格</span>
<span class="value">Typecho Theme VOID</span> <span class="value">Typecho Theme VOID</span>
@@ -40,6 +86,42 @@
<span class="value">Cloudflare Access (Zero Trust)</span> <span class="value">Cloudflare Access (Zero Trust)</span>
</div> </div>
</div> </div>
{/if}
{#if activeTab === 'terms'}
<div class="settings-card full-width">
<h3>个性化术语设定</h3>
<p class="desc">您可以根据部署场景(如教学系统、文档库)自定义后台各模块的显示名称,例如将“文章”改为“实验手册”。</p>
<form method="POST" action="?/updateAliases" use:enhance class="alias-form">
<div class="form-row">
<div class="form-group">
<label for="alias_post">文章</label>
<input type="text" id="alias_post" name="alias_post" value={data.settings['alias.post'] || '文章'} required>
</div>
<div class="form-group">
<label for="alias_page">页面</label>
<input type="text" id="alias_page" name="alias_page" value={data.settings['alias.page'] || '页面'} required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="alias_category">分类</label>
<input type="text" id="alias_category" name="alias_category" value={data.settings['alias.category'] || '分类'} required>
</div>
<div class="form-group">
<label for="alias_tag">标签</label>
<input type="text" id="alias_tag" name="alias_tag" value={data.settings['alias.tag'] || '标签'} required>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
{/if}
</div> </div>
<style> <style>
@@ -55,6 +137,53 @@
color: var(--text-color); color: var(--text-color);
} }
.tabs-header {
display: flex;
gap: 20px;
margin-bottom: 30px;
border-bottom: 2px solid var(--border-color, rgba(0,0,0,0.1));
}
.tab-btn {
background: none;
border: none;
padding: 10px 15px;
font-size: 1rem;
font-weight: 600;
color: var(--text-muted, rgba(0,0,0,0.5));
cursor: pointer;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
transition: all 0.2s;
}
.tab-btn:hover {
color: var(--text-color);
}
.tab-btn.active {
color: var(--text-color);
border-bottom-color: var(--accent-color, #419b38);
}
.success-alert {
padding: 12px 16px;
background-color: transparent;
border-left: 4px solid #419b38;
color: #419b38;
margin-bottom: 30px;
font-weight: 600;
}
.error-alert {
padding: 12px 16px;
background-color: transparent;
border-left: 4px solid #d9534f;
color: #d9534f;
margin-bottom: 30px;
font-weight: 600;
}
.settings-container { .settings-container {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
@@ -62,6 +191,10 @@
align-items: stretch; align-items: stretch;
} }
.full-width {
grid-column: 1 / -1;
}
.settings-card { .settings-card {
border: 2px solid var(--text-color); border: 2px solid var(--text-color);
padding: 24px; padding: 24px;
@@ -71,13 +204,20 @@
} }
.settings-card h3 { .settings-card h3 {
margin: 0 0 18px 0; margin: 0 0 12px 0;
font-size: 1.2rem; font-size: 1.2rem;
border-bottom: 1px solid var(--border-color, rgba(0,0,0,0.1)); border-bottom: 1px solid var(--border-color, rgba(0,0,0,0.1));
padding-bottom: 10px; padding-bottom: 10px;
color: var(--text-color); color: var(--text-color);
} }
.settings-card .desc {
font-size: 0.9rem;
color: var(--text-muted, rgba(0,0,0,0.6));
margin-bottom: 24px;
line-height: 1.5;
}
.info-row { .info-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -105,4 +245,82 @@
text-align: right; text-align: right;
word-break: break-word; word-break: break-word;
} }
.alias-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-row {
display: flex;
gap: 20px;
}
@media (max-width: 768px) {
.form-row {
flex-direction: column;
gap: 20px;
}
}
.form-group {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-weight: 600;
font-size: 0.9rem;
color: var(--text-color);
}
.form-group input {
padding: 10px 14px;
border: 1px solid rgba(0, 0, 0, 0.15);
background: rgba(0, 0, 0, 0.02);
font-size: 1rem;
color: var(--text-color);
border-radius: 0;
transition: all 0.2s;
}
.form-group input:focus {
outline: none;
border-color: var(--accent-color, #419b38);
box-shadow: 0 0 0 2px rgba(65, 155, 56, 0.1);
}
.form-actions {
margin-top: 10px;
display: flex;
justify-content: flex-end;
}
.btn {
display: inline-block;
padding: 10px 24px;
text-decoration: none;
font-size: 0.95rem;
font-weight: 600;
transition: all 0.3s ease;
cursor: pointer;
border: 2px solid var(--text-color);
background-color: transparent;
color: var(--text-color);
font-family: inherit;
}
.btn-primary {
background-color: var(--text-color);
color: var(--bg-color, #ffffff);
}
.btn-primary:hover {
background-color: var(--accent-color, #419b38);
border-color: var(--accent-color, #419b38);
color: white;
}
</style> </style>