From 7bbaf67591ba30cfff79c2a15fb00538caa2044a Mon Sep 17 00:00:00 2001 From: "DESKTOP-72TV0V4\\caoxiaozhu" Date: Sat, 21 Mar 2026 11:33:42 +0800 Subject: [PATCH] feat(settings): refactor LLM config to table inline-edit UI - Remove old card-based model list UI - Add LLM config state management (expandedRow, editingSnapshot) - Implement addModel/removeModel with embedding/rerank constraints - Implement updateModel, testModel, saveModel, toggleRow, cancelEdit - Add showRequiredWarning computed property - Rewrite template with 4 LLM type sections using LLMTableRow - Add styles for llm-type-section, warning-bar, etc. - Update loadSettings to initialize with empty arrays Co-Authored-By: Claude Opus 4.6 --- frontend/src/views/SettingsView.vue | 369 +++++++++++++++++----------- 1 file changed, 221 insertions(+), 148 deletions(-) diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 744f400..9358b9b 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -2,20 +2,31 @@ import { ref, onMounted, computed } from 'vue' import { settingsApi, type LLMConfig, type SchedulerConfig, type LLMModelConfig, type LLMProvider } from '@/api/settings' import LLMTableRow from '@/components/settings/LLMTableRow.vue' -import { Save, Eye, EyeOff, Play, RotateCcw, Plus, Trash2, X, Check } from 'lucide-vue-next' +import { Save, RotateCcw, Plus } from 'lucide-vue-next' // 状态 const loading = ref(false) const saving = ref(false) -const showApiKey = ref>({}) const savingModel = ref(null) // 当前正在保存的模型 key -const modelSaveSuccess = ref(null) // 刚刚保存成功的模型 key const toast = ref<{ show: boolean; message: string; type: 'success' | 'error' }>({ show: false, message: '', type: 'success' }) +// 展开的行 +const expandedRow = ref(null) // 'chat-0', 'vlm-0' 等 + +// 当前正在编辑的模型快照(用于取消时恢复) +const editingSnapshot = ref<{ type: string; index: number; data: LLMModelConfig } | null>(null) + +// 必填警告 +const showRequiredWarning = computed(() => { + return llmConfig.value.chat.length === 0 || + llmConfig.value.embedding.length === 0 || + llmConfig.value.rerank.length === 0 +}) + // 用户资料 const profile = ref({ email: '', @@ -69,11 +80,6 @@ function isModelDirty(type: string, index: number): boolean { return JSON.stringify(original) !== JSON.stringify(current) } -// 获取模型唯一标识 -function getModelKey(type: string, index: number): string { - return `${type}-${index}` -} - // 创建空的模型配置 function createEmptyModel(type: string): LLMModelConfig { return { @@ -91,19 +97,64 @@ function addModel(type: string) { if (!llmConfig.value[type as keyof LLMConfig]) { llmConfig.value[type as keyof LLMConfig] = [] } - llmConfig.value[type as keyof LLMConfig]!.push(createEmptyModel(type)) + // embedding/rerank 最多 1 个 + if ((type === 'embedding' || type === 'rerank') && + llmConfig.value[type as keyof LLMConfig]!.length >= 1) { + showToast(`${type === 'embedding' ? 'Embedding' : 'Rerank'} 最多配置 1 个`, 'error') + return + } + const newModel = createEmptyModel(type) + llmConfig.value[type as keyof LLMConfig]!.push(newModel) + // 自动展开新添加的行 + const newIndex = llmConfig.value[type as keyof LLMConfig]!.length - 1 + expandedRow.value = getRowKey(type, newIndex) + editingSnapshot.value = { type, index: newIndex, data: JSON.parse(JSON.stringify(newModel)) } } // 删除模型 function removeModel(type: string, index: number) { + // embedding/rerank 为知识库必填,至少保留 1 个 + if ((type === 'embedding' || type === 'rerank') && + llmConfig.value[type as keyof LLMConfig]!.length <= 1) { + showToast(`${type === 'embedding' ? 'Embedding' : 'Rerank'} 为知识库必填,至少保留 1 个`, 'error') + return + } llmConfig.value[type as keyof LLMConfig]!.splice(index, 1) + expandedRow.value = null + editingSnapshot.value = null } -// 复制模型 -function duplicateModel(type: string, index: number) { - const model = llmConfig.value[type as keyof LLMConfig]![index] - const copy = { ...model, name: `${model.name}-copy-${Date.now()}` } - llmConfig.value[type as keyof LLMConfig]!.push(copy) +// 行标识 +function getRowKey(type: string, index: number): string { + return `${type}-${index}` +} + +// 切换行展开 +function toggleRow(type: string, index: number, model: LLMModelConfig) { + const key = getRowKey(type, index) + if (expandedRow.value === key) { + expandedRow.value = null + editingSnapshot.value = null + } else { + // 保存快照用于取消 + editingSnapshot.value = { type, index, data: JSON.parse(JSON.stringify(model)) } + expandedRow.value = key + } +} + +// 取消编辑 +function cancelEdit(type: string, index: number) { + if (editingSnapshot.value && editingSnapshot.value.type === type && editingSnapshot.value.index === index) { + // 恢复原始数据 + llmConfig.value[type as keyof LLMConfig]![index] = editingSnapshot.value.data + } + expandedRow.value = null + editingSnapshot.value = null +} + +// 更新模型 +function updateModel(type: string, index: number, model: LLMModelConfig) { + llmConfig.value[type as keyof LLMConfig]![index] = model } // 加载设置 @@ -119,16 +170,15 @@ async function loadSettings() { originalProfile.value = { ...profile.value } // 加载 LLM 配置 - if (res.data.llm_config && Object.keys(res.data.llm_config).length > 0) { - llmConfig.value = res.data.llm_config as LLMConfig - } else { - // 默认添加一个空配置 + if (res.data.llm_config) { llmConfig.value = { - chat: [createEmptyModel('chat')], - vlm: [createEmptyModel('vlm')], - embedding: [createEmptyModel('embedding')], - rerank: [createEmptyModel('rerank')] + chat: res.data.llm_config.chat || [], + vlm: res.data.llm_config.vlm || [], + embedding: res.data.llm_config.embedding || [], + rerank: res.data.llm_config.rerank || [] } + } else { + llmConfig.value = { chat: [], vlm: [], embedding: [], rerank: [] } } originalLlmConfig.value = JSON.parse(JSON.stringify(llmConfig.value)) @@ -180,7 +230,7 @@ async function saveLLM() { // 保存单个模型 async function saveModel(type: string, index: number) { - const key = getModelKey(type, index) + const key = getRowKey(type, index) savingModel.value = key try { // 发送完整的配置(包含该类型的所有模型) @@ -189,13 +239,10 @@ async function saveModel(type: string, index: number) { // 更新原始配置(深拷贝当前完整配置) originalLlmConfig.value = JSON.parse(JSON.stringify(llmConfig.value)) - // 显示成功状态 - modelSaveSuccess.value = key - setTimeout(() => { - if (modelSaveSuccess.value === key) { - modelSaveSuccess.value = null - } - }, 1500) + // 关闭展开的行 + expandedRow.value = null + editingSnapshot.value = null + showToast('保存成功') } catch (e: unknown) { const msg = (e as { response?: { data?: { detail?: string } } })?.response?.data?.detail || '保存失败' showToast(msg, 'error') @@ -205,15 +252,19 @@ async function saveModel(type: string, index: number) { } // 测试 LLM 连接 -async function testLLM(type: string, config: LLMModelConfig) { +async function testModel(type: string, index: number, model: LLMModelConfig) { try { - const res = await settingsApi.testLLM({ type: type as any, ...config }) + const res = await settingsApi.testLLM({ type: type as any, ...model }) if (res.data.success) { - showToast(`连接成功: ${res.data.message}`) + // 测试通过,标记为可用 + llmConfig.value[type as keyof LLMConfig]![index].enabled = true + showToast('连接成功') } else { + llmConfig.value[type as keyof LLMConfig]![index].enabled = false showToast(`连接失败: ${res.data.error}`, 'error') } } catch (e) { + llmConfig.value[type as keyof LLMConfig]![index].enabled = false showToast('测试连接失败', 'error') } } @@ -273,8 +324,6 @@ function showToast(message: string, type: 'success' | 'error' = 'success') { }, 3000) } -// LLM 类型列表 -const llmTypes = ['chat', 'vlm', 'embedding', 'rerank'] as const // 获取模型显示名称 function getModelName(config: LLMModelConfig): string { @@ -344,133 +393,109 @@ onMounted(loadSettings) -
+
- {{ type.toUpperCase() }} - + // LLM CONFIGURATION
- -
-
-
-
- - -
-
- - -
-
+ +
+ ⚠ chat / embedding / rerank 为知识库必填,请确保已配置 +
-
-
- - -
-
- - -
-
- -
- - +
+
+ CHAT + +
+
+
+
+
+
暂无 chat 模型配置
+
-
- -
- - -
-
- -