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";
|
import { hashPassword } from "./security.mjs";
|
||||||
|
|
||||||
const defaultAdminEmail = process.env.CMS_ADMIN_EMAIL || "admin@example.com";
|
const defaultAdminEmail = process.env.CMS_ADMIN_EMAIL || "admin@example.com";
|
||||||
@@ -33,7 +33,9 @@ function seedSettings() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Object.entries(seeds).forEach(([key, value]) => {
|
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"));
|
setSetting(key, value, key.includes("secret") || key.includes("password"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+64
-5
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from "vue";
|
import { computed, onMounted, reactive, ref } from "vue";
|
||||||
import { loadCurrentUser, loginWithEmail, logoutCms } from "./api/auth";
|
import { loadCurrentUser, loginWithEmail, logoutCms } from "./api/auth";
|
||||||
|
import { loadSystemSettings, saveSystemSettings } from "./api/adminSettings";
|
||||||
import {
|
import {
|
||||||
createGitHubRepository,
|
createGitHubRepository,
|
||||||
loadGitHubRepositories,
|
loadGitHubRepositories,
|
||||||
@@ -28,10 +29,21 @@ import ContentPanel from "./components/ContentPanel.vue";
|
|||||||
import LoginView from "./components/LoginView.vue";
|
import LoginView from "./components/LoginView.vue";
|
||||||
import PublishPanel from "./components/PublishPanel.vue";
|
import PublishPanel from "./components/PublishPanel.vue";
|
||||||
import StructurePanel from "./components/StructurePanel.vue";
|
import StructurePanel from "./components/StructurePanel.vue";
|
||||||
|
import SystemSettingsPanel from "./components/SystemSettingsPanel.vue";
|
||||||
import { mockNavItems, mockPages, mockSidebarGroups, mockSiteSettings } from "./data/mockSite";
|
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: "account", label: "用户中心", icon: "U" },
|
||||||
{ key: "connect", label: "仓库连接", icon: "G" },
|
{ key: "connect", label: "仓库连接", icon: "G" },
|
||||||
{ key: "content", label: "页面管理", icon: "P" },
|
{ key: "content", label: "页面管理", icon: "P" },
|
||||||
@@ -40,6 +52,13 @@ const panels: Array<{ key: PanelKey; label: string; icon: string }> = [
|
|||||||
{ key: "publish", label: "发布中心", icon: "R" },
|
{ key: "publish", label: "发布中心", icon: "R" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (currentUser.value?.role === "system_admin") {
|
||||||
|
basePanels.push({ key: "admin", label: "系统设置", icon: "A" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return basePanels;
|
||||||
|
});
|
||||||
|
|
||||||
const githubConnection = reactive<GitHubConnection>({
|
const githubConnection = reactive<GitHubConnection>({
|
||||||
token: "",
|
token: "",
|
||||||
owner: "",
|
owner: "",
|
||||||
@@ -60,6 +79,7 @@ const sidebarGroups = reactive(
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
const siteSettings = reactive({ ...mockSiteSettings });
|
const siteSettings = reactive({ ...mockSiteSettings });
|
||||||
|
const systemSettings = ref<SystemSetting[]>([]);
|
||||||
const activePageId = ref("");
|
const activePageId = ref("");
|
||||||
const settingsSha = ref<string>();
|
const settingsSha = ref<string>();
|
||||||
const isLoadingPages = ref(false);
|
const isLoadingPages = ref(false);
|
||||||
@@ -67,6 +87,7 @@ const isConnecting = ref(false);
|
|||||||
const isLoadingRepos = ref(false);
|
const isLoadingRepos = ref(false);
|
||||||
const pageSaveState = ref<"idle" | "saving" | "saved" | "error">("idle");
|
const pageSaveState = ref<"idle" | "saving" | "saved" | "error">("idle");
|
||||||
const settingsSaveState = 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 githubUser = ref<GitHubUser | null>(null);
|
||||||
const currentUser = ref<CmsUser | null>(null);
|
const currentUser = ref<CmsUser | null>(null);
|
||||||
const repositories = ref<GitHubRepository[]>([]);
|
const repositories = ref<GitHubRepository[]>([]);
|
||||||
@@ -75,11 +96,12 @@ const loginError = ref("");
|
|||||||
const activityItems = ref(["等待读取站点"]);
|
const activityItems = ref(["等待读取站点"]);
|
||||||
|
|
||||||
const activePage = computed(() => pages.find((page) => page.id === activePageId.value) ?? pages[0]);
|
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(() => {
|
const repoLabel = computed(() => {
|
||||||
if (dataSource.value === "github" && hasGitHubConnection.value) {
|
if (dataSource.value === "github" && hasGitHubConnection.value) {
|
||||||
return `${githubConnection.owner}/${githubConnection.repo}`;
|
return `${githubConnection.owner}/${githubConnection.repo}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "test-vitepress";
|
return "test-vitepress";
|
||||||
});
|
});
|
||||||
const repoBranch = computed(() =>
|
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() {
|
async function reloadSite() {
|
||||||
await Promise.all([loadPages(), loadSettings()]);
|
await Promise.all([loadPages(), loadSettings()]);
|
||||||
}
|
}
|
||||||
@@ -208,7 +243,7 @@ async function handleEmailLogin(payload: { email: string; password: string }) {
|
|||||||
try {
|
try {
|
||||||
currentUser.value = await loginWithEmail(payload.email, payload.password);
|
currentUser.value = await loginWithEmail(payload.email, payload.password);
|
||||||
await loadAuthState();
|
await loadAuthState();
|
||||||
await reloadSite();
|
await Promise.all([reloadSite(), loadAdminSettings()]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
loginError.value = error instanceof Error ? error.message : "登录失败";
|
loginError.value = error instanceof Error ? error.message : "登录失败";
|
||||||
} finally {
|
} finally {
|
||||||
@@ -221,6 +256,7 @@ async function handleLogout() {
|
|||||||
currentUser.value = null;
|
currentUser.value = null;
|
||||||
githubUser.value = null;
|
githubUser.value = null;
|
||||||
repositories.value = [];
|
repositories.value = [];
|
||||||
|
systemSettings.value = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function connectGithub(connection: GitHubConnection) {
|
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() {
|
function publishChanges() {
|
||||||
activityItems.value = ["已创建 commit,并触发部署流程", ...activityItems.value];
|
activityItems.value = ["已创建 commit,并触发部署流程", ...activityItems.value];
|
||||||
}
|
}
|
||||||
@@ -366,7 +419,7 @@ onMounted(async () => {
|
|||||||
await loadAuthState();
|
await loadAuthState();
|
||||||
|
|
||||||
if (currentUser.value) {
|
if (currentUser.value) {
|
||||||
await reloadSite();
|
await Promise.all([reloadSite(), loadAdminSettings()]);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isAuthLoading.value = false;
|
isAuthLoading.value = false;
|
||||||
@@ -417,6 +470,12 @@ onMounted(async () => {
|
|||||||
@github-logout="signOutGithub"
|
@github-logout="signOutGithub"
|
||||||
@logout="handleLogout"
|
@logout="handleLogout"
|
||||||
/>
|
/>
|
||||||
|
<SystemSettingsPanel
|
||||||
|
v-else-if="activePanel === 'admin'"
|
||||||
|
:save-state="systemSettingsSaveState"
|
||||||
|
:settings="systemSettings"
|
||||||
|
@save-settings="saveAdminSettings"
|
||||||
|
/>
|
||||||
<ConnectPanel
|
<ConnectPanel
|
||||||
v-else-if="activePanel === 'connect'"
|
v-else-if="activePanel === 'connect'"
|
||||||
:connection="githubConnection"
|
: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 EditorMode = "write" | "preview";
|
||||||
export type DataSource = "local" | "github";
|
export type DataSource = "local" | "github";
|
||||||
|
|
||||||
@@ -69,3 +69,10 @@ export interface GitHubRepository {
|
|||||||
defaultBranch: string;
|
defaultBranch: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SystemSetting {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
encrypted: boolean;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user