feat(admin): 添加后台显示设置功能

- 新增后台显示设置页面,允许自定义布局标题
- 在布局组件中动态显示自定义标题
- 在后端添加对应的 API 端点用于获取和保存显示配置
- 移除无用的 Demo.vue 组件文件
This commit is contained in:
anghunk
2026-02-02 16:42:03 +08:00
parent abeb7f2d41
commit 4231928f9f
6 changed files with 161 additions and 13 deletions

View File

@@ -128,6 +128,10 @@ export type FeatureSettingsResponse = {
enableArticleLike: boolean;
};
export type AdminDisplaySettingsResponse = {
layoutTitle: string | null;
};
export async function loginAdmin(name: string, password: string): Promise<string> {
const res = await post<AdminLoginResponse>('/admin/login', { name, password });
const key = res.data.key;
@@ -327,6 +331,16 @@ export function saveFeatureSettings(data: { enableCommentLike?: boolean; enableA
return put<{ message: string }>('/admin/settings/features', data);
}
export function fetchAdminDisplaySettings(): Promise<AdminDisplaySettingsResponse> {
return get<AdminDisplaySettingsResponse>('/admin/settings/admin-display');
}
export function saveAdminDisplaySettings(data: {
layoutTitle?: string;
}): Promise<{ message: string }> {
return put<{ message: string }>('/admin/settings/admin-display', data);
}
export type TelegramSettingsResponse = {
botToken: string | null;
chatId: string | null;

View File

@@ -1,3 +1,7 @@
h1 {
color: red;
}
.page {
display: flex;
flex-direction: column;

View File

@@ -9,7 +9,7 @@
>
<PhTextIndent :size="20" />
</button>
<div class="layout-title">CWD 评论系统</div>
<div class="layout-title">{{ layoutTitle }}</div>
<div class="layout-actions-wrapper">
<div class="layout-domain-filter layout-domain-filter-header">
<select v-model="domainFilter" class="layout-domain-select">
@@ -173,7 +173,7 @@
<script setup lang="ts">
import { ref, onMounted, watch, provide, computed } from "vue";
import { useRouter, useRoute } from "vue-router";
import { logoutAdmin, fetchDomainList } from "../../api/admin";
import { logoutAdmin, fetchDomainList, fetchAdminDisplaySettings } from "../../api/admin";
import { useTheme } from "../../composables/useTheme";
import packageJson from "../../../package.json";
@@ -191,6 +191,7 @@ const apiVersion = ref("");
const checkedApiBaseUrl = ref("");
const apiVersionError = ref("");
const versionModalVisible = ref(false);
const layoutTitle = ref("CWD 评论系统");
const themeTitle = computed(() => {
if (theme.value === "light") return "明亮模式";
@@ -254,11 +255,21 @@ async function loadVersion() {
}
}
async function loadDisplaySettings() {
try {
const res = await fetchAdminDisplaySettings();
layoutTitle.value = res.layoutTitle || "CWD 评论系统";
} catch {
layoutTitle.value = "CWD 评论系统";
}
}
provide("domainFilter", domainFilter);
onMounted(() => {
loadDomains();
loadVersion();
loadDisplaySettings();
});
watch(domainFilter, (value) => {

View File

@@ -93,11 +93,7 @@
</label>
<div class="tag-input">
<div class="tag-input-inner">
<span
v-for="ip in blockedIpTags"
:key="ip"
class="tag-input-tag"
>
<span v-for="ip in blockedIpTags" :key="ip" class="tag-input-tag">
<span class="tag-input-tag-text">{{ ip }}</span>
<button
type="button"
@@ -191,6 +187,33 @@
</transition>
</div>
<div class="card">
<div class="card-header" @click="toggleCard('display')">
<div class="card-title">后台显示设置</div>
<div class="card-icon" :class="{ expanded: cardsExpanded.display }"></div>
</div>
<transition name="collapse">
<div v-show="cardsExpanded.display" class="card-body">
<div class="form-item">
<label class="form-label">后台标题</label>
<input
v-model="adminLayoutTitle"
class="form-input"
type="text"
placeholder="CWD 评论系统"
/>
<div class="form-hint">显示在后台顶部导航栏左侧</div>
</div>
<div class="card-actions">
<button class="card-button" :disabled="savingDisplay" @click="saveDisplay">
<span v-if="savingDisplay">保存中...</span>
<span v-else>保存</span>
</button>
</div>
</div>
</transition>
</div>
<div class="card">
<div class="card-header" @click="toggleCard('email')">
<div class="card-title">通知邮箱设置</div>
@@ -422,8 +445,10 @@ import {
fetchEmailNotifySettings,
saveEmailNotifySettings,
sendTestEmail,
fetchFeatureSettings,
saveFeatureSettings,
fetchFeatureSettings,
saveFeatureSettings,
fetchAdminDisplaySettings,
saveAdminDisplaySettings,
fetchTelegramSettings,
saveTelegramSettings,
setupTelegramWebhook,
@@ -510,6 +535,7 @@ const commentAdminEmail = ref("");
const commentAdminBadge = ref("");
const avatarPrefix = ref("");
const commentAdminEnabled = ref(false);
const adminLayoutTitle = ref("CWD 评论系统");
const allowedDomainTags = ref<string[]>([]);
const allowedDomainInput = ref("");
const blockedIpTags = ref<string[]>([]);
@@ -661,6 +687,7 @@ const savingEmail = ref(false);
const testingEmail = ref(false);
const savingComment = ref(false);
const savingFeature = ref(false);
const savingDisplay = ref(false);
const loading = ref(false);
const message = ref("");
const messageType = ref<"success" | "error">("success");
@@ -674,12 +701,18 @@ function loadCardsExpanded() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
return JSON.parse(saved);
const parsed = JSON.parse(saved);
return {
comment: parsed.comment ?? true,
display: parsed.display ?? false,
feature: parsed.feature ?? false,
email: parsed.email ?? false,
telegram: parsed.telegram ?? false,
};
}
} catch {
// 忽略错误
}
return { comment: true, feature: false, email: false, telegram: false };
return { comment: true, display: false, feature: false, email: false, telegram: false };
}
const cardsExpanded = ref(loadCardsExpanded());
@@ -731,11 +764,12 @@ function resetTemplatesToDefault() {
async function load() {
loading.value = true;
try {
const [commentRes, emailNotifyRes, featureRes, telegramRes] = await Promise.all([
const [commentRes, emailNotifyRes, featureRes, telegramRes, displayRes] = await Promise.all([
fetchCommentSettings(),
fetchEmailNotifySettings(),
fetchFeatureSettings(),
fetchTelegramSettings(),
fetchAdminDisplaySettings(),
]);
commentAdminEmail.value = commentRes.adminEmail || "";
commentAdminBadge.value = commentRes.adminBadge ?? "";
@@ -767,6 +801,8 @@ async function load() {
telegramChatId.value = telegramRes.chatId || "";
telegramNotifyEnabled.value = telegramRes.notifyEnabled;
adminLayoutTitle.value = displayRes.layoutTitle || "CWD 评论系统";
if (emailNotifyRes.templates) {
templateAdmin.value = emailNotifyRes.templates.admin || DEFAULT_ADMIN_TEMPLATE;
templateReply.value = emailNotifyRes.templates.reply || DEFAULT_REPLY_TEMPLATE;
@@ -917,6 +953,23 @@ async function saveFeature() {
}
}
async function saveDisplay() {
savingDisplay.value = true;
message.value = "";
try {
const res = await saveAdminDisplaySettings({
layoutTitle: adminLayoutTitle.value,
});
showToast(res.message || "保存成功", "success");
} catch (e: any) {
message.value = e.message || "保存失败";
messageType.value = "error";
} finally {
savingDisplay.value = false;
}
}
async function saveTelegram() {
savingTelegram.value = true;
message.value = "";

View File

@@ -49,6 +49,11 @@ const COMMENT_ADMIN_KEY_HASH_KEY = 'comment_admin_key_hash';
const COMMENT_REQUIRE_REVIEW_KEY = 'comment_require_review';
const COMMENT_BLOCKED_IPS_KEY = 'comment_blocked_ips';
const COMMENT_BLOCKED_EMAILS_KEY = 'comment_blocked_emails';
const ADMIN_DISPLAY_CONFIG_KEY = 'admin_display_config';
type AdminDisplaySettings = {
layoutTitle: string;
};
async function loadCommentSettings(env: Bindings) {
await env.CWD_DB.prepare('CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)').run();
@@ -119,6 +124,43 @@ async function loadCommentSettings(env: Bindings) {
};
}
async function loadAdminDisplaySettings(env: Bindings): Promise<AdminDisplaySettings> {
await env.CWD_DB.prepare('CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)').run();
const row = await env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
.bind(ADMIN_DISPLAY_CONFIG_KEY)
.first<{ value: string }>();
const defaults: AdminDisplaySettings = {
layoutTitle: 'CWD 评论系统',
};
if (!row || !row.value) {
return defaults;
}
try {
const parsed = JSON.parse(row.value) as Partial<AdminDisplaySettings>;
const result: AdminDisplaySettings = {
layoutTitle: typeof parsed.layoutTitle === 'string' && parsed.layoutTitle ? parsed.layoutTitle : defaults.layoutTitle,
};
return result;
} catch {
return defaults;
}
}
async function saveAdminDisplaySettings(env: Bindings, settings: Partial<AdminDisplaySettings>) {
await env.CWD_DB.prepare('CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)').run();
const current = await loadAdminDisplaySettings(env);
const next: AdminDisplaySettings = {
layoutTitle: settings.layoutTitle !== undefined ? settings.layoutTitle : current.layoutTitle,
};
const value = JSON.stringify(next);
await env.CWD_DB.prepare('REPLACE INTO Settings (key, value) VALUES (?, ?)').bind(ADMIN_DISPLAY_CONFIG_KEY, value).run();
}
async function saveCommentSettings(
env: Bindings,
settings: {
@@ -291,6 +333,30 @@ app.put('/admin/settings/telegram', updateTelegramSettings);
app.post('/admin/settings/telegram/setup', setupTelegramWebhook);
app.post('/admin/settings/telegram/test', testTelegramMessage);
app.get('/admin/settings/admin-display', async (c) => {
try {
const settings = await loadAdminDisplaySettings(c.env);
return c.json(settings);
} catch (e: any) {
return c.json({ message: e.message || '加载显示配置失败' }, 500);
}
});
app.put('/admin/settings/admin-display', async (c) => {
try {
const body = await c.req.json();
const layoutTitle = typeof body.layoutTitle === 'string' ? body.layoutTitle : undefined;
await saveAdminDisplaySettings(c.env, {
layoutTitle,
});
return c.json({ message: '保存成功' });
} catch (e: any) {
return c.json({ message: e.message || '保存失败' }, 500);
}
});
app.get('/admin/settings/comments', async (c) => {
try {
const settings = await loadCommentSettings(c.env);