feat: 系统设置优化

This commit is contained in:
tbphp
2025-07-03 22:38:01 +08:00
parent c0594d068e
commit 21fe7cca04
8 changed files with 163 additions and 66 deletions

View File

@@ -17,18 +17,22 @@ func GetSettings(c *gin.Context) {
currentSettings := settingsManager.GetSettings() currentSettings := settingsManager.GetSettings()
settingsInfo := config.GenerateSettingsMetadata(&currentSettings) settingsInfo := config.GenerateSettingsMetadata(&currentSettings)
// Group settings by category // Group settings by category while preserving order
categorized := make(map[string][]models.SystemSettingInfo) categorized := make(map[string][]models.SystemSettingInfo)
var categoryOrder []string
for _, s := range settingsInfo { for _, s := range settingsInfo {
if _, exists := categorized[s.Category]; !exists {
categoryOrder = append(categoryOrder, s.Category)
}
categorized[s.Category] = append(categorized[s.Category], s) categorized[s.Category] = append(categorized[s.Category], s)
} }
// Create the response structure // Create the response structure in the correct order
var responseData []models.CategorizedSettings var responseData []models.CategorizedSettings
for categoryName, settings := range categorized { for _, categoryName := range categoryOrder {
responseData = append(responseData, models.CategorizedSettings{ responseData = append(responseData, models.CategorizedSettings{
CategoryName: categoryName, CategoryName: categoryName,
Settings: settings, Settings: categorized[categoryName],
}) })
} }

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import GlobalProviders from "@/components/GlobalProviders.vue";
import Layout from "@/components/Layout.vue"; import Layout from "@/components/Layout.vue";
import { useAuthKey } from "@/services/auth"; import { useAuthKey } from "@/services/auth";
import { NDialogProvider, NMessageProvider } from "naive-ui";
import { computed } from "vue"; import { computed } from "vue";
const authKey = useAuthKey(); const authKey = useAuthKey();
@@ -9,10 +9,8 @@ const isLoggedIn = computed(() => !!authKey.value);
</script> </script>
<template> <template>
<n-message-provider> <global-providers>
<n-dialog-provider> <layout v-if="isLoggedIn" />
<layout v-if="isLoggedIn" /> <router-view v-else />
<router-view v-else /> </global-providers>
</n-dialog-provider>
</n-message-provider>
</template> </template>

27
web/src/api/settings.ts Normal file
View File

@@ -0,0 +1,27 @@
import http from "@/utils/http";
export interface Setting {
key: string;
name: string;
value: string | number;
type: "int" | "string";
min_value?: number;
description: string;
}
export interface SettingCategory {
category_name: string;
settings: Setting[];
}
export type SettingsUpdatePayload = Record<string, string | number>;
export const settingsApi = {
getSettings: async (): Promise<SettingCategory[]> => {
const response = await http.get("/settings");
return response.data || [];
},
updateSettings: (data: SettingsUpdatePayload): Promise<void> => {
return http.put("/settings", data);
},
};

View File

@@ -0,0 +1,51 @@
<script setup lang="ts">
import { appState } from "@/utils/app-state";
import {
NDialogProvider,
NLoadingBarProvider,
NMessageProvider,
useLoadingBar,
useMessage,
} from "naive-ui";
import { defineComponent, watch } from "vue";
function useGlobalMessage() {
window.$message = useMessage();
}
const LoadingBar = defineComponent({
setup() {
const loadingBar = useLoadingBar();
watch(
() => appState.loading,
loading => {
if (loading) {
loadingBar.start();
} else {
loadingBar.finish();
}
}
);
return () => null;
},
});
const Message = defineComponent({
setup() {
useGlobalMessage();
return () => null;
},
});
</script>
<template>
<n-loading-bar-provider>
<n-message-provider>
<n-dialog-provider>
<slot />
<loading-bar />
<message />
</n-dialog-provider>
</n-message-provider>
</n-loading-bar-provider>
</template>

9
web/src/types/env.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
/// <reference types="vite/client" />
import type { MessageApiInjection } from "naive-ui/es/message/src/MessageProvider";
declare global {
interface Window {
$message: MessageApiInjection;
}
}

View File

@@ -0,0 +1,9 @@
import { reactive } from "vue";
interface AppState {
loading: boolean;
}
export const appState = reactive<AppState>({
loading: false,
});

View File

@@ -1,5 +1,6 @@
import { useAuthService } from "@/services/auth"; import { useAuthService } from "@/services/auth";
import axios from "axios"; import axios from "axios";
import { appState } from "./app-state";
const http = axios.create({ const http = axios.create({
baseURL: "/api", baseURL: "/api",
@@ -9,6 +10,7 @@ const http = axios.create({
// 请求拦截器 // 请求拦截器
http.interceptors.request.use(config => { http.interceptors.request.use(config => {
appState.loading = true;
const authKey = localStorage.getItem("authKey"); const authKey = localStorage.getItem("authKey");
if (authKey) { if (authKey) {
config.headers.Authorization = `Bearer ${authKey}`; config.headers.Authorization = `Bearer ${authKey}`;
@@ -18,12 +20,30 @@ http.interceptors.request.use(config => {
// 响应拦截器 // 响应拦截器
http.interceptors.response.use( http.interceptors.response.use(
response => response.data, response => {
appState.loading = false;
if (response.config.method !== "get") {
window.$message.success("操作成功");
}
return response.data;
},
error => { error => {
if (error.response && error.response.status === 401) { appState.loading = false;
const { logout } = useAuthService(); if (error.response) {
logout(); // The request was made and the server responded with a status code
window.location.href = "/login"; // that falls out of the range of 2xx
if (error.response.status === 401) {
const { logout } = useAuthService();
logout();
window.location.href = "/login";
}
window.$message.error(error.response.data?.message || `请求失败: ${error.response.status}`);
} else if (error.request) {
// The request was made but no response was received
window.$message.error("网络错误,请检查您的连接");
} else {
// Something happened in setting up the request that triggered an Error
window.$message.error("请求设置错误");
} }
console.error("API Error:", error); console.error("API Error:", error);
return Promise.reject(error); return Promise.reject(error);

View File

@@ -1,37 +1,19 @@
<script setup lang="ts"> <script setup lang="ts">
import http from "@/utils/http"; import { settingsApi, type SettingCategory } from "@/api/settings";
import type { FormValidationError } from "naive-ui"; import { NTooltip } from "naive-ui";
import { ref } from "vue"; import { ref } from "vue";
interface Setting {
key: string;
name: string;
value: string | number;
type: "int" | "string";
min_value?: number;
}
interface SettingCategory {
category_name: string;
settings: Setting[];
}
const settingList = ref<SettingCategory[]>([]); const settingList = ref<SettingCategory[]>([]);
const loading = ref(false);
const formRef = ref(); const formRef = ref();
const form = ref<Record<string, string | number>>({}); const form = ref<Record<string, string | number>>({});
const isSaving = ref(false);
fetchSettings(); fetchSettings();
async function fetchSettings() { async function fetchSettings() {
loading.value = true; const data = await settingsApi.getSettings();
try { settingList.value = data || [];
const response = await http.get("/settings"); initForm();
settingList.value = response.data || [];
initForm();
} finally {
loading.value = false;
}
} }
function initForm() { function initForm() {
@@ -43,36 +25,25 @@ function initForm() {
}, {}); }, {});
} }
function handleSubmit() { async function handleSubmit() {
if (loading.value) { if (isSaving.value) {
return; return;
} }
formRef.value.validate(async (errors: Array<FormValidationError> | undefined) => { try {
if (errors) { await formRef.value.validate();
return; isSaving.value = true;
} await settingsApi.updateSettings(form.value);
await fetchSettings();
try { } finally {
loading.value = true; isSaving.value = false;
await http.put("/settings", form.value); }
fetchSettings();
} finally {
loading.value = false;
}
});
} }
</script> </script>
<template> <template>
<n-spin :show="loading"> <div>
<n-form <n-form ref="formRef" :model="form" label-placement="left" label-width="110">
ref="formRef"
:model="form"
label-placement="left"
label-width="110"
:disabled="loading"
>
<n-card <n-card
v-for="(category, cIndex) in settingList" v-for="(category, cIndex) in settingList"
:key="cIndex" :key="cIndex"
@@ -83,7 +54,6 @@ function handleSubmit() {
<n-form-item <n-form-item
v-for="item in category.settings" v-for="item in category.settings"
:key="item.key" :key="item.key"
:label="item.name"
:path="item.key" :path="item.key"
style="margin-right: 10px" style="margin-right: 10px"
:rule="{ :rule="{
@@ -91,6 +61,14 @@ function handleSubmit() {
message: `请输入${item.name}`, message: `请输入${item.name}`,
}" }"
> >
<template #label>
<n-tooltip trigger="hover">
<template #trigger>
<span>{{ item.name }}</span>
</template>
<span>{{ item.description }}</span>
</n-tooltip>
</template>
<n-input-number <n-input-number
v-if="item.type === 'int'" v-if="item.type === 'int'"
v-model:value="form[item.key]" v-model:value="form[item.key]"
@@ -110,13 +88,14 @@ function handleSubmit() {
</n-space> </n-space>
</n-card> </n-card>
</n-form> </n-form>
</n-spin> </div>
<n-flex justify="center"> <n-flex justify="center">
<n-button <n-button
v-show="settingList.length > 0" v-show="settingList.length > 0"
type="primary" type="primary"
style="width: 200px" style="width: 200px"
:loading="loading" :loading="isSaving"
:disabled="isSaving"
@click="handleSubmit" @click="handleSubmit"
> >
保存设置 保存设置