feat: 系统设置优化
This commit is contained in:
@@ -17,18 +17,22 @@ func GetSettings(c *gin.Context) {
|
|||||||
currentSettings := settingsManager.GetSettings()
|
currentSettings := settingsManager.GetSettings()
|
||||||
settingsInfo := config.GenerateSettingsMetadata(¤tSettings)
|
settingsInfo := config.GenerateSettingsMetadata(¤tSettings)
|
||||||
|
|
||||||
// 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],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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
27
web/src/api/settings.ts
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
51
web/src/components/GlobalProviders.vue
Normal file
51
web/src/components/GlobalProviders.vue
Normal 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
9
web/src/types/env.d.ts
vendored
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
9
web/src/utils/app-state.ts
Normal file
9
web/src/utils/app-state.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { reactive } from "vue";
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const appState = reactive<AppState>({
|
||||||
|
loading: false,
|
||||||
|
});
|
@@ -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);
|
||||||
|
@@ -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"
|
||||||
>
|
>
|
||||||
保存设置
|
保存设置
|
||||||
|
Reference in New Issue
Block a user