feat(admin): 添加后台显示设置功能
- 新增后台显示设置页面,允许自定义布局标题 - 在布局组件中动态显示自定义标题 - 在后端添加对应的 API 端点用于获取和保存显示配置 - 移除无用的 Demo.vue 组件文件
This commit is contained in:
@@ -128,6 +128,10 @@ export type FeatureSettingsResponse = {
|
|||||||
enableArticleLike: boolean;
|
enableArticleLike: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AdminDisplaySettingsResponse = {
|
||||||
|
layoutTitle: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export async function loginAdmin(name: string, password: string): Promise<string> {
|
export async function loginAdmin(name: string, password: string): Promise<string> {
|
||||||
const res = await post<AdminLoginResponse>('/admin/login', { name, password });
|
const res = await post<AdminLoginResponse>('/admin/login', { name, password });
|
||||||
const key = res.data.key;
|
const key = res.data.key;
|
||||||
@@ -327,6 +331,16 @@ export function saveFeatureSettings(data: { enableCommentLike?: boolean; enableA
|
|||||||
return put<{ message: string }>('/admin/settings/features', data);
|
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 = {
|
export type TelegramSettingsResponse = {
|
||||||
botToken: string | null;
|
botToken: string | null;
|
||||||
chatId: string | null;
|
chatId: string | null;
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
h1 {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
>
|
>
|
||||||
<PhTextIndent :size="20" />
|
<PhTextIndent :size="20" />
|
||||||
</button>
|
</button>
|
||||||
<div class="layout-title">CWD 评论系统</div>
|
<div class="layout-title">{{ layoutTitle }}</div>
|
||||||
<div class="layout-actions-wrapper">
|
<div class="layout-actions-wrapper">
|
||||||
<div class="layout-domain-filter layout-domain-filter-header">
|
<div class="layout-domain-filter layout-domain-filter-header">
|
||||||
<select v-model="domainFilter" class="layout-domain-select">
|
<select v-model="domainFilter" class="layout-domain-select">
|
||||||
@@ -173,7 +173,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, watch, provide, computed } from "vue";
|
import { ref, onMounted, watch, provide, computed } from "vue";
|
||||||
import { useRouter, useRoute } from "vue-router";
|
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 { useTheme } from "../../composables/useTheme";
|
||||||
import packageJson from "../../../package.json";
|
import packageJson from "../../../package.json";
|
||||||
|
|
||||||
@@ -191,6 +191,7 @@ const apiVersion = ref("");
|
|||||||
const checkedApiBaseUrl = ref("");
|
const checkedApiBaseUrl = ref("");
|
||||||
const apiVersionError = ref("");
|
const apiVersionError = ref("");
|
||||||
const versionModalVisible = ref(false);
|
const versionModalVisible = ref(false);
|
||||||
|
const layoutTitle = ref("CWD 评论系统");
|
||||||
|
|
||||||
const themeTitle = computed(() => {
|
const themeTitle = computed(() => {
|
||||||
if (theme.value === "light") return "明亮模式";
|
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);
|
provide("domainFilter", domainFilter);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadDomains();
|
loadDomains();
|
||||||
loadVersion();
|
loadVersion();
|
||||||
|
loadDisplaySettings();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(domainFilter, (value) => {
|
watch(domainFilter, (value) => {
|
||||||
|
|||||||
@@ -93,11 +93,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<div class="tag-input">
|
<div class="tag-input">
|
||||||
<div class="tag-input-inner">
|
<div class="tag-input-inner">
|
||||||
<span
|
<span v-for="ip in blockedIpTags" :key="ip" class="tag-input-tag">
|
||||||
v-for="ip in blockedIpTags"
|
|
||||||
:key="ip"
|
|
||||||
class="tag-input-tag"
|
|
||||||
>
|
|
||||||
<span class="tag-input-tag-text">{{ ip }}</span>
|
<span class="tag-input-tag-text">{{ ip }}</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -191,6 +187,33 @@
|
|||||||
</transition>
|
</transition>
|
||||||
</div>
|
</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">
|
||||||
<div class="card-header" @click="toggleCard('email')">
|
<div class="card-header" @click="toggleCard('email')">
|
||||||
<div class="card-title">通知邮箱设置</div>
|
<div class="card-title">通知邮箱设置</div>
|
||||||
@@ -422,8 +445,10 @@ import {
|
|||||||
fetchEmailNotifySettings,
|
fetchEmailNotifySettings,
|
||||||
saveEmailNotifySettings,
|
saveEmailNotifySettings,
|
||||||
sendTestEmail,
|
sendTestEmail,
|
||||||
fetchFeatureSettings,
|
fetchFeatureSettings,
|
||||||
saveFeatureSettings,
|
saveFeatureSettings,
|
||||||
|
fetchAdminDisplaySettings,
|
||||||
|
saveAdminDisplaySettings,
|
||||||
fetchTelegramSettings,
|
fetchTelegramSettings,
|
||||||
saveTelegramSettings,
|
saveTelegramSettings,
|
||||||
setupTelegramWebhook,
|
setupTelegramWebhook,
|
||||||
@@ -510,6 +535,7 @@ const commentAdminEmail = ref("");
|
|||||||
const commentAdminBadge = ref("");
|
const commentAdminBadge = ref("");
|
||||||
const avatarPrefix = ref("");
|
const avatarPrefix = ref("");
|
||||||
const commentAdminEnabled = ref(false);
|
const commentAdminEnabled = ref(false);
|
||||||
|
const adminLayoutTitle = ref("CWD 评论系统");
|
||||||
const allowedDomainTags = ref<string[]>([]);
|
const allowedDomainTags = ref<string[]>([]);
|
||||||
const allowedDomainInput = ref("");
|
const allowedDomainInput = ref("");
|
||||||
const blockedIpTags = ref<string[]>([]);
|
const blockedIpTags = ref<string[]>([]);
|
||||||
@@ -661,6 +687,7 @@ const savingEmail = ref(false);
|
|||||||
const testingEmail = ref(false);
|
const testingEmail = ref(false);
|
||||||
const savingComment = ref(false);
|
const savingComment = ref(false);
|
||||||
const savingFeature = ref(false);
|
const savingFeature = ref(false);
|
||||||
|
const savingDisplay = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const message = ref("");
|
const message = ref("");
|
||||||
const messageType = ref<"success" | "error">("success");
|
const messageType = ref<"success" | "error">("success");
|
||||||
@@ -674,12 +701,18 @@ function loadCardsExpanded() {
|
|||||||
try {
|
try {
|
||||||
const saved = localStorage.getItem(STORAGE_KEY);
|
const saved = localStorage.getItem(STORAGE_KEY);
|
||||||
if (saved) {
|
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 {
|
} 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());
|
const cardsExpanded = ref(loadCardsExpanded());
|
||||||
@@ -731,11 +764,12 @@ function resetTemplatesToDefault() {
|
|||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const [commentRes, emailNotifyRes, featureRes, telegramRes] = await Promise.all([
|
const [commentRes, emailNotifyRes, featureRes, telegramRes, displayRes] = await Promise.all([
|
||||||
fetchCommentSettings(),
|
fetchCommentSettings(),
|
||||||
fetchEmailNotifySettings(),
|
fetchEmailNotifySettings(),
|
||||||
fetchFeatureSettings(),
|
fetchFeatureSettings(),
|
||||||
fetchTelegramSettings(),
|
fetchTelegramSettings(),
|
||||||
|
fetchAdminDisplaySettings(),
|
||||||
]);
|
]);
|
||||||
commentAdminEmail.value = commentRes.adminEmail || "";
|
commentAdminEmail.value = commentRes.adminEmail || "";
|
||||||
commentAdminBadge.value = commentRes.adminBadge ?? "";
|
commentAdminBadge.value = commentRes.adminBadge ?? "";
|
||||||
@@ -767,6 +801,8 @@ async function load() {
|
|||||||
telegramChatId.value = telegramRes.chatId || "";
|
telegramChatId.value = telegramRes.chatId || "";
|
||||||
telegramNotifyEnabled.value = telegramRes.notifyEnabled;
|
telegramNotifyEnabled.value = telegramRes.notifyEnabled;
|
||||||
|
|
||||||
|
adminLayoutTitle.value = displayRes.layoutTitle || "CWD 评论系统";
|
||||||
|
|
||||||
if (emailNotifyRes.templates) {
|
if (emailNotifyRes.templates) {
|
||||||
templateAdmin.value = emailNotifyRes.templates.admin || DEFAULT_ADMIN_TEMPLATE;
|
templateAdmin.value = emailNotifyRes.templates.admin || DEFAULT_ADMIN_TEMPLATE;
|
||||||
templateReply.value = emailNotifyRes.templates.reply || DEFAULT_REPLY_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() {
|
async function saveTelegram() {
|
||||||
savingTelegram.value = true;
|
savingTelegram.value = true;
|
||||||
message.value = "";
|
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_REQUIRE_REVIEW_KEY = 'comment_require_review';
|
||||||
const COMMENT_BLOCKED_IPS_KEY = 'comment_blocked_ips';
|
const COMMENT_BLOCKED_IPS_KEY = 'comment_blocked_ips';
|
||||||
const COMMENT_BLOCKED_EMAILS_KEY = 'comment_blocked_emails';
|
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) {
|
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();
|
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(
|
async function saveCommentSettings(
|
||||||
env: Bindings,
|
env: Bindings,
|
||||||
settings: {
|
settings: {
|
||||||
@@ -291,6 +333,30 @@ app.put('/admin/settings/telegram', updateTelegramSettings);
|
|||||||
app.post('/admin/settings/telegram/setup', setupTelegramWebhook);
|
app.post('/admin/settings/telegram/setup', setupTelegramWebhook);
|
||||||
app.post('/admin/settings/telegram/test', testTelegramMessage);
|
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) => {
|
app.get('/admin/settings/comments', async (c) => {
|
||||||
try {
|
try {
|
||||||
const settings = await loadCommentSettings(c.env);
|
const settings = await loadCommentSettings(c.env);
|
||||||
|
|||||||
Reference in New Issue
Block a user