feat: add system settings panel
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
});
|
||||
|
||||
+65
-6
@@ -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 }> = [
|
||||
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"
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user