feat: add system settings panel

This commit is contained in:
Coldsmile_7
2026-06-08 22:50:18 +08:00
parent f7a3501262
commit d5f7e06e4e
5 changed files with 245 additions and 15 deletions
+4 -2
View File
@@ -1,4 +1,4 @@
import { db, getSetting, migrate, setSetting } from "./db.mjs";
import { db, migrate, setSetting } from "./db.mjs";
import { hashPassword } from "./security.mjs";
const defaultAdminEmail = process.env.CMS_ADMIN_EMAIL || "admin@example.com";
@@ -33,7 +33,9 @@ function seedSettings() {
};
Object.entries(seeds).forEach(([key, value]) => {
if (getSetting(key, undefined) === undefined) {
const existing = db.prepare("SELECT key FROM system_settings WHERE key = ?").get(key);
if (!existing) {
setSetting(key, value, key.includes("secret") || key.includes("password"));
}
});
+71 -12
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue";
import { loadCurrentUser, loginWithEmail, logoutCms } from "./api/auth";
import { loadSystemSettings, saveSystemSettings } from "./api/adminSettings";
import {
createGitHubRepository,
loadGitHubRepositories,
@@ -28,17 +29,35 @@ import ContentPanel from "./components/ContentPanel.vue";
import LoginView from "./components/LoginView.vue";
import PublishPanel from "./components/PublishPanel.vue";
import StructurePanel from "./components/StructurePanel.vue";
import SystemSettingsPanel from "./components/SystemSettingsPanel.vue";
import { mockNavItems, mockPages, mockSidebarGroups, mockSiteSettings } from "./data/mockSite";
import type { CmsUser, DataSource, DocPage, GitHubConnection, GitHubRepository, GitHubUser, PanelKey } from "./types";
import type {
CmsUser,
DataSource,
DocPage,
GitHubConnection,
GitHubRepository,
GitHubUser,
PanelKey,
SystemSetting,
} from "./types";
const panels: Array<{ key: PanelKey; label: string; icon: string }> = [
{ key: "account", label: "用户中心", icon: "U" },
{ key: "connect", label: "仓库连接", icon: "G" },
{ key: "content", label: "页面管理", icon: "P" },
{ key: "structure", label: "站点结构", icon: "S" },
{ key: "config", label: "主题配置", icon: "C" },
{ key: "publish", label: "发布中心", icon: "R" },
];
const panels = computed<Array<{ key: PanelKey; label: string; icon: string }>>(() => {
const basePanels: Array<{ key: PanelKey; label: string; icon: string }> = [
{ key: "account", label: "用户中心", icon: "U" },
{ key: "connect", label: "仓库连接", icon: "G" },
{ key: "content", label: "页面管理", icon: "P" },
{ key: "structure", label: "站点结构", icon: "S" },
{ key: "config", label: "主题配置", icon: "C" },
{ key: "publish", label: "发布中心", icon: "R" },
];
if (currentUser.value?.role === "system_admin") {
basePanels.push({ key: "admin", label: "系统设置", icon: "A" });
}
return basePanels;
});
const githubConnection = reactive<GitHubConnection>({
token: "",
@@ -60,6 +79,7 @@ const sidebarGroups = reactive(
})),
);
const siteSettings = reactive({ ...mockSiteSettings });
const systemSettings = ref<SystemSetting[]>([]);
const activePageId = ref("");
const settingsSha = ref<string>();
const isLoadingPages = ref(false);
@@ -67,6 +87,7 @@ const isConnecting = ref(false);
const isLoadingRepos = ref(false);
const pageSaveState = ref<"idle" | "saving" | "saved" | "error">("idle");
const settingsSaveState = ref<"idle" | "saving" | "saved" | "error">("idle");
const systemSettingsSaveState = ref<"idle" | "saving" | "saved" | "error">("idle");
const githubUser = ref<GitHubUser | null>(null);
const currentUser = ref<CmsUser | null>(null);
const repositories = ref<GitHubRepository[]>([]);
@@ -75,11 +96,12 @@ const loginError = ref("");
const activityItems = ref(["等待读取站点"]);
const activePage = computed(() => pages.find((page) => page.id === activePageId.value) ?? pages[0]);
const activePanelTitle = computed(() => panels.find((panel) => panel.key === activePanel.value)?.label ?? "");
const activePanelTitle = computed(() => panels.value.find((panel) => panel.key === activePanel.value)?.label ?? "");
const repoLabel = computed(() => {
if (dataSource.value === "github" && hasGitHubConnection.value) {
return `${githubConnection.owner}/${githubConnection.repo}`;
}
return "test-vitepress";
});
const repoBranch = computed(() =>
@@ -165,6 +187,19 @@ async function loadSettings() {
}
}
async function loadAdminSettings() {
if (currentUser.value?.role !== "system_admin") return;
try {
systemSettings.value = await loadSystemSettings();
} catch (error) {
activityItems.value = [
error instanceof Error ? error.message : "读取系统设置失败",
...activityItems.value,
];
}
}
async function reloadSite() {
await Promise.all([loadPages(), loadSettings()]);
}
@@ -208,7 +243,7 @@ async function handleEmailLogin(payload: { email: string; password: string }) {
try {
currentUser.value = await loginWithEmail(payload.email, payload.password);
await loadAuthState();
await reloadSite();
await Promise.all([reloadSite(), loadAdminSettings()]);
} catch (error) {
loginError.value = error instanceof Error ? error.message : "登录失败";
} finally {
@@ -221,6 +256,7 @@ async function handleLogout() {
currentUser.value = null;
githubUser.value = null;
repositories.value = [];
systemSettings.value = [];
}
async function connectGithub(connection: GitHubConnection) {
@@ -357,6 +393,23 @@ async function saveSettings() {
}
}
async function saveAdminSettings(settings: SystemSetting[]) {
systemSettingsSaveState.value = "saving";
try {
await saveSystemSettings(settings);
systemSettingsSaveState.value = "saved";
await loadAdminSettings();
activityItems.value = ["已保存系统设置", ...activityItems.value];
} catch (error) {
systemSettingsSaveState.value = "error";
activityItems.value = [
error instanceof Error ? error.message : "保存系统设置失败",
...activityItems.value,
];
}
}
function publishChanges() {
activityItems.value = ["已创建 commit,并触发部署流程", ...activityItems.value];
}
@@ -366,7 +419,7 @@ onMounted(async () => {
await loadAuthState();
if (currentUser.value) {
await reloadSite();
await Promise.all([reloadSite(), loadAdminSettings()]);
}
} finally {
isAuthLoading.value = false;
@@ -417,6 +470,12 @@ onMounted(async () => {
@github-logout="signOutGithub"
@logout="handleLogout"
/>
<SystemSettingsPanel
v-else-if="activePanel === 'admin'"
:save-state="systemSettingsSaveState"
:settings="systemSettings"
@save-settings="saveAdminSettings"
/>
<ConnectPanel
v-else-if="activePanel === 'connect'"
:connection="githubConnection"
+30
View File
@@ -0,0 +1,30 @@
import type { SystemSetting } from "../types";
interface SettingsResponse {
settings: SystemSetting[];
}
export async function loadSystemSettings() {
const response = await fetch("/api/admin/settings");
if (!response.ok) {
throw new Error(`读取系统设置失败:${response.status}`);
}
const data = (await response.json()) as SettingsResponse;
return data.settings;
}
export async function saveSystemSettings(settings: SystemSetting[]) {
const response = await fetch("/api/admin/settings", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ settings }),
});
if (!response.ok) {
throw new Error(`保存系统设置失败:${response.status}`);
}
}
+132
View File
@@ -0,0 +1,132 @@
<script setup lang="ts">
import { computed, reactive, watch } from "vue";
import type { SystemSetting } from "../types";
const props = defineProps<{
settings: SystemSetting[];
saveState: "idle" | "saving" | "saved" | "error";
}>();
defineEmits<{
saveSettings: [settings: SystemSetting[]];
}>();
const draft = reactive<Record<string, SystemSetting>>({});
const groups = [
{
title: "基础",
eyebrow: "general",
keys: ["site.title"],
},
{
title: "认证",
eyebrow: "auth",
keys: ["auth.allow_email_login", "auth.allow_github_login", "auth.allow_registration"],
},
{
title: "SMTP",
eyebrow: "mail",
keys: ["smtp.host", "smtp.port", "smtp.username", "smtp.password", "smtp.sender_email"],
},
{
title: "GitHub",
eyebrow: "github",
keys: ["github.api_base_url", "github.web_base_url", "github.oauth_client_id", "github.oauth_client_secret"],
},
];
const labels: Record<string, string> = {
"site.title": "CMS 标题",
"auth.allow_email_login": "允许邮箱登录",
"auth.allow_github_login": "允许 GitHub 登录",
"auth.allow_registration": "开放注册",
"smtp.host": "SMTP 地址",
"smtp.port": "SMTP 端口",
"smtp.username": "SMTP 用户名",
"smtp.password": "SMTP 密码",
"smtp.sender_email": "发件人邮箱",
"github.api_base_url": "GitHub API 地址",
"github.web_base_url": "GitHub Web 地址",
"github.oauth_client_id": "OAuth Client ID",
"github.oauth_client_secret": "OAuth Client Secret",
};
const booleanKeys = new Set([
"auth.allow_email_login",
"auth.allow_github_login",
"auth.allow_registration",
]);
watch(
() => props.settings,
(settings) => {
Object.keys(draft).forEach((key) => delete draft[key]);
settings.forEach((setting) => {
draft[setting.key] = { ...setting };
});
},
{ immediate: true },
);
const draftSettings = computed(() => Object.values(draft));
function getSetting(key: string) {
if (!draft[key]) {
draft[key] = {
key,
value: "",
encrypted: key.includes("password") || key.includes("secret"),
updatedAt: "",
};
}
return draft[key];
}
function isBooleanKey(key: string) {
return booleanKeys.has(key);
}
</script>
<template>
<section class="panel is-visible">
<div class="settings-toolbar">
<p class="save-status" :class="`is-${saveState}`">
<template v-if="saveState === 'saved'">系统设置已保存</template>
<template v-else-if="saveState === 'error'">保存失败请查看日志</template>
<template v-else-if="saveState === 'saving'">正在保存系统设置</template>
<template v-else>系统设置保存到 SQLite 数据库</template>
</p>
<button class="primary-button" type="button" @click="$emit('saveSettings', draftSettings)">
{{ saveState === "saving" ? "保存中" : "保存设置" }}
</button>
</div>
<div class="settings-grid">
<section v-for="group in groups" :key="group.title" class="management-card">
<p class="eyebrow">{{ group.eyebrow }}</p>
<h3>{{ group.title }}</h3>
<label
v-for="key in group.keys"
:key="key"
:class="{ 'toggle-row': isBooleanKey(key) }"
>
<span>{{ labels[key] || key }}</span>
<input
v-if="isBooleanKey(key)"
:checked="getSetting(key).value === 'true'"
type="checkbox"
@change="getSetting(key).value = ($event.target as HTMLInputElement).checked ? 'true' : 'false'"
/>
<input
v-else
v-model="getSetting(key).value"
:placeholder="getSetting(key).encrypted ? '留空表示不修改敏感值' : ''"
:type="getSetting(key).encrypted ? 'password' : 'text'"
/>
</label>
</section>
</div>
</section>
</template>
+8 -1
View File
@@ -1,4 +1,4 @@
export type PanelKey = "account" | "connect" | "content" | "structure" | "config" | "publish";
export type PanelKey = "account" | "admin" | "connect" | "content" | "structure" | "config" | "publish";
export type EditorMode = "write" | "preview";
export type DataSource = "local" | "github";
@@ -69,3 +69,10 @@ export interface GitHubRepository {
defaultBranch: string;
updatedAt: string;
}
export interface SystemSetting {
key: string;
value: string;
encrypted: boolean;
updatedAt: string;
}