feat(admin): 添加后台显示设置功能
- 新增后台显示设置页面,允许自定义布局标题 - 在布局组件中动态显示自定义标题 - 在后端添加对应的 API 端点用于获取和保存显示配置 - 移除无用的 Demo.vue 组件文件
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
h1 {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user