feat: 大量优化和bug修复
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
<!-- app/components/layout/SettingsDialog.vue - 右上角设置弹窗:个人 API 配置 + superadmin 系统配置 -->
|
||||
<script lang="ts" setup>
|
||||
import { Settings } from "lucide-vue-next";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useSettingsStore } from "@/stores/settings";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { userSettings, systemConfig, userLoading, userFetching, sysLoading } =
|
||||
storeToRefs(settingsStore);
|
||||
|
||||
const open = ref(false);
|
||||
const isSuperAdmin = computed(() => authStore.user?.role === "superadmin");
|
||||
const userFormBusy = computed(() => userLoading.value || userFetching.value);
|
||||
|
||||
// ─────────────────────────────────────
|
||||
// 个人配置表单状态
|
||||
// ─────────────────────────────────────
|
||||
const userForm = reactive({
|
||||
useCustomConfig: false,
|
||||
customApiBase: "",
|
||||
customApiKey: "", // 空 = 不修改
|
||||
customModel: "",
|
||||
customMaxTokens: "" as string,
|
||||
customTemperature: "" as string
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────
|
||||
// 系统配置表单状态
|
||||
// ─────────────────────────────────────
|
||||
const sysForm = reactive({
|
||||
openAiApiBase: "",
|
||||
openAiApiKey: "", // 空 = 不修改
|
||||
openAiModel: "",
|
||||
maxTokens: "" as string,
|
||||
temperature: "" as string,
|
||||
maxSseLineBytes: "" as string
|
||||
});
|
||||
|
||||
/** 打开时拉取最新配置,并回填表单 */
|
||||
watch(open, async (val) => {
|
||||
if (!val) return;
|
||||
await settingsStore.fetchUserSettings();
|
||||
if (isSuperAdmin.value) {
|
||||
await settingsStore.fetchSystemConfig();
|
||||
}
|
||||
});
|
||||
|
||||
/** 服务端数据加载后同步到表单 */
|
||||
watch(
|
||||
userSettings,
|
||||
(s) => {
|
||||
if (!s) return;
|
||||
userForm.useCustomConfig = s.useCustomConfig;
|
||||
userForm.customApiBase = s.customApiBase ?? "";
|
||||
userForm.customApiKey = ""; // key 不回填,placeholder 展示遮蔽串
|
||||
userForm.customModel = s.customModel ?? "";
|
||||
userForm.customMaxTokens =
|
||||
s.customMaxTokens !== null ? String(s.customMaxTokens) : "";
|
||||
userForm.customTemperature =
|
||||
s.customTemperature !== null ? String(s.customTemperature) : "";
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
systemConfig,
|
||||
(s) => {
|
||||
if (!s) return;
|
||||
sysForm.openAiApiBase = s.openAiApiBase;
|
||||
sysForm.openAiApiKey = ""; // key 不回填
|
||||
sysForm.openAiModel = s.openAiModel;
|
||||
sysForm.maxTokens = String(s.maxTokens);
|
||||
sysForm.temperature = String(s.temperature);
|
||||
sysForm.maxSseLineBytes = String(s.maxSseLineBytes);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/** 保存个人配置 */
|
||||
const saveUserSettings = async () => {
|
||||
const maxTokens = userForm.customMaxTokens
|
||||
? Number(userForm.customMaxTokens)
|
||||
: null;
|
||||
const temperature = userForm.customTemperature
|
||||
? Number(userForm.customTemperature)
|
||||
: null;
|
||||
|
||||
await settingsStore.updateUserSettings({
|
||||
useCustomConfig: userForm.useCustomConfig,
|
||||
customApiBase: userForm.customApiBase,
|
||||
customApiKey: userForm.customApiKey, // 空字符串 = 服务端跳过
|
||||
customModel: userForm.customModel,
|
||||
customMaxTokens: maxTokens,
|
||||
customTemperature: temperature
|
||||
});
|
||||
};
|
||||
|
||||
/** 保存系统配置(superadmin only) */
|
||||
const saveSystemConfig = async () => {
|
||||
await settingsStore.updateSystemConfig({
|
||||
openAiApiBase: sysForm.openAiApiBase || undefined,
|
||||
openAiApiKey: sysForm.openAiApiKey || undefined, // 空 = 不更新
|
||||
openAiModel: sysForm.openAiModel || undefined,
|
||||
maxTokens: sysForm.maxTokens ? Number(sysForm.maxTokens) : undefined,
|
||||
temperature: sysForm.temperature ? Number(sysForm.temperature) : undefined,
|
||||
maxSseLineBytes: sysForm.maxSseLineBytes
|
||||
? Number(sysForm.maxSseLineBytes)
|
||||
: undefined
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogTrigger as-child>
|
||||
<Button variant="ghost" size="icon" aria-label="设置">
|
||||
<Settings class="size-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent class="max-h-[90vh] overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
设置
|
||||
<!-- 弹窗打开时拉取配置的加载指示,与保存操作无关 -->
|
||||
<span
|
||||
v-if="userFetching"
|
||||
class="size-3.5 animate-spin rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground"
|
||||
/>
|
||||
</DialogTitle>
|
||||
<!-- 为无障碍语义提供描述,视觉上隐藏 -->
|
||||
<DialogDescription class="sr-only">
|
||||
管理您的自定义 API 配置
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<!-- ───────── 个人 API 配置区 ───────── -->
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm font-medium text-foreground">个人 API 配置</p>
|
||||
<p class="text-xs text-muted-foreground leading-relaxed">
|
||||
启用后将使用您的自定义 API 地址和 Key 进行答题,API Key
|
||||
和地址必须同时填写才会生效。
|
||||
</p>
|
||||
|
||||
<!-- 启用/禁用开关 -->
|
||||
<div class="flex items-center gap-3">
|
||||
<Switch
|
||||
:id="'use-custom-config'"
|
||||
v-model="userForm.useCustomConfig"
|
||||
:disabled="userFormBusy"
|
||||
/>
|
||||
<Label for="use-custom-config" class="cursor-pointer select-none">
|
||||
使用自定义 API
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<!-- 自定义字段,关闭时视觉置灰但仍可编辑 -->
|
||||
<div
|
||||
class="space-y-3 transition-opacity"
|
||||
:class="{ 'opacity-40': !userForm.useCustomConfig }"
|
||||
>
|
||||
<div class="space-y-1.5">
|
||||
<Label for="custom-api-base">API 地址</Label>
|
||||
<Input
|
||||
id="custom-api-base"
|
||||
v-model="userForm.customApiBase"
|
||||
placeholder="https://api.openai.com/v1"
|
||||
autocomplete="off"
|
||||
:disabled="userFormBusy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="custom-api-key">API Key</Label>
|
||||
<Input
|
||||
id="custom-api-key"
|
||||
v-model="userForm.customApiKey"
|
||||
type="password"
|
||||
:placeholder="
|
||||
userSettings?.customApiKeyPreview
|
||||
? `已设置(${userSettings.customApiKeyPreview}),留空不修改`
|
||||
: '请输入 API Key'
|
||||
"
|
||||
autocomplete="new-password"
|
||||
:disabled="userFormBusy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="custom-model">
|
||||
模型名称
|
||||
<span class="text-muted-foreground font-normal"
|
||||
>(可选,留空跟随系统配置)</span
|
||||
>
|
||||
</Label>
|
||||
<Input
|
||||
id="custom-model"
|
||||
v-model="userForm.customModel"
|
||||
placeholder="gpt-5.2"
|
||||
:disabled="userFormBusy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="custom-max-tokens">
|
||||
最大 Token 数
|
||||
<span class="text-muted-foreground font-normal">(可选)</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="custom-max-tokens"
|
||||
v-model="userForm.customMaxTokens"
|
||||
type="number"
|
||||
min="1"
|
||||
max="32768"
|
||||
placeholder="跟随系统配置"
|
||||
:disabled="userFormBusy"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label for="custom-temperature">
|
||||
温度
|
||||
<span class="text-muted-foreground font-normal">(可选)</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="custom-temperature"
|
||||
v-model="userForm.customTemperature"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="2"
|
||||
placeholder="跟随系统配置"
|
||||
:disabled="userFormBusy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
class="w-full"
|
||||
:disabled="
|
||||
userFormBusy ||
|
||||
!userForm.customApiBase ||
|
||||
(!userForm.customApiKey && !userSettings?.customApiKeyPreview)
|
||||
"
|
||||
@click="saveUserSettings"
|
||||
>
|
||||
{{ userLoading ? "保存中..." : "保存个人配置" }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- ───────── 系统配置区(仅 superadmin) ───────── -->
|
||||
<template v-if="isSuperAdmin">
|
||||
<hr class="my-2 border-border" />
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">系统配置</p>
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
修改后立即对所有用户(未启用自定义配置的)生效。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="sys-api-base">API 地址</Label>
|
||||
<Input
|
||||
id="sys-api-base"
|
||||
v-model="sysForm.openAiApiBase"
|
||||
placeholder="https://api.openai.com/v1"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="sys-api-key">API Key</Label>
|
||||
<Input
|
||||
id="sys-api-key"
|
||||
v-model="sysForm.openAiApiKey"
|
||||
type="password"
|
||||
:placeholder="
|
||||
systemConfig?.openAiApiKeyPreview
|
||||
? `已设置(${systemConfig.openAiApiKeyPreview}),留空不修改`
|
||||
: '请输入 API Key'
|
||||
"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="sys-model">模型名称</Label>
|
||||
<Input
|
||||
id="sys-model"
|
||||
v-model="sysForm.openAiModel"
|
||||
placeholder="gpt-5.2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="sys-max-tokens">最大 Token 数</Label>
|
||||
<Input
|
||||
id="sys-max-tokens"
|
||||
v-model="sysForm.maxTokens"
|
||||
type="number"
|
||||
min="1"
|
||||
max="32768"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label for="sys-temperature">温度</Label>
|
||||
<Input
|
||||
id="sys-temperature"
|
||||
v-model="sysForm.temperature"
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="sys-max-sse">SSE 单行最大字节数</Label>
|
||||
<Input
|
||||
id="sys-max-sse"
|
||||
v-model="sysForm.maxSseLineBytes"
|
||||
type="number"
|
||||
min="1024"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
class="w-full"
|
||||
variant="secondary"
|
||||
:disabled="sysLoading"
|
||||
@click="saveSystemConfig"
|
||||
>
|
||||
{{ sysLoading ? "更新中..." : "更新系统配置" }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
Reference in New Issue
Block a user