From 96dcb5d20e7762dc110b0ae9548623facce16dc9 Mon Sep 17 00:00:00 2001 From: ViperEkura <3081035982@qq.com> Date: Wed, 15 Apr 2026 13:24:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=20=E4=BC=98=E5=8C=96=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dashboard/src/views/SettingsView.vue | 197 ++++++++++++++------------- luxx/routes/providers.py | 6 +- 2 files changed, 104 insertions(+), 99 deletions(-) diff --git a/dashboard/src/views/SettingsView.vue b/dashboard/src/views/SettingsView.vue index e4674dc..f3004cd 100644 --- a/dashboard/src/views/SettingsView.vue +++ b/dashboard/src/views/SettingsView.vue @@ -63,9 +63,13 @@
-
默认 Provider
+
+ 默认 Provider + 选择默认使用的 LLM Provider +
- + @@ -75,19 +79,13 @@
温度 (Temperature)
-
- - 控制随机性,较低值更确定,较高值更有创造性 -
+
最大 Tokens
-
- - 单次回复最大 token 数 -
+
@@ -130,59 +128,61 @@
🔌 LLM Provider -
-
-
加载中...
-
{{ error }}
-
-

暂无 Provider,点击上方按钮添加

-
- -
- - - - - - - - - - - - - - - - - -
名称API / 模型启用操作
-
{{ p.name }}
-
- 默认 - 启用 - 禁用 -
-
-
{{ p.base_url }}
-
模型: {{ p.default_model }}
-
最大Tokens: {{ p.max_tokens || 8192 }}
-
- - -
- - - -
-
+
加载中...
+
{{ error }}
+
+ + + + + + + + + + + + + + + + + +
名称API / 模型启用操作
+
{{ p.name }}
+
+ 默认 + 启用 + 禁用 +
+
+
{{ p.base_url }}
+
模型: {{ p.default_model }}
+
最大Tokens: {{ p.max_tokens || 8192 }}
+
+ + +
+ + + +
+
+
+ + +
+
+ +
+
@@ -238,18 +238,6 @@ 单次回复最大 token 数,默认 8192
-
- -
{{ formError }}
@@ -376,7 +364,7 @@ const testResult = ref(null) const formError = ref('') const form = ref({ - name: '', base_url: '', api_key: '', default_model: '', max_tokens: 8192, is_default: false + name: '', base_url: '', api_key: '', default_model: '', max_tokens: 8192 }) const fetchProviders = async () => { @@ -399,7 +387,7 @@ const fetchProviders = async () => { const closeModal = () => { showModal.value = false editing.value = null - form.value = { name: '', base_url: '', api_key: '', default_model: '', max_tokens: 8192, is_default: false } + form.value = { name: '', base_url: '', api_key: '', default_model: '', max_tokens: 8192 } formError.value = '' } @@ -411,10 +399,9 @@ const editProvider = async (p) => { form.value = { name: res.data.name, base_url: res.data.base_url, - api_key: res.data.api_key || '', + api_key: '', // 不显示原密码 default_model: res.data.default_model, - max_tokens: res.data.max_tokens || 8192, - is_default: res.data.is_default + max_tokens: res.data.max_tokens || 8192 } } } catch (e) { @@ -424,8 +411,13 @@ const editProvider = async (p) => { } const saveProvider = async () => { - if (!form.value.base_url || !form.value.api_key || !form.value.default_model) { - formError.value = '请填写所有必填项' + if (!form.value.base_url || !form.value.default_model) { + formError.value = '请填写必填项(Base URL 和模型名称)' + return + } + // 编辑时 api_key 可以为空(表示不修改密码) + if (!editing.value && !form.value.api_key) { + formError.value = '请填写 API Key' return } @@ -434,8 +426,11 @@ const saveProvider = async () => { try { const data = { ...form.value } let res - if (editing.value) res = await providersAPI.update(editing.value, data) - else res = await providersAPI.create(data) + if (editing.value) { + res = await providersAPI.update(editing.value, data) + } else { + res = await providersAPI.create(data) + } if (res.success) { closeModal(); fetchProviders() } else throw new Error(res.message) @@ -482,6 +477,22 @@ const toggleEnabled = async (p) => { } catch (e) { alert('更新失败: ' + e.message) } } +const saveDefaultProvider = async () => { + try { + // 取消所有 Provider 的默认状态 + for (const p of providers.value) { + if (p.is_default && p.id !== modelSettings.value.default_provider) { + await providersAPI.update(p.id, { is_default: false }) + } + } + // 设置选中的 Provider 为默认 + if (modelSettings.value.default_provider) { + await providersAPI.update(modelSettings.value.default_provider, { is_default: true }) + } + await fetchProviders() + } catch (e) { alert('设置默认 Provider 失败: ' + e.message) } +} + onMounted(() => { fetchUserInfo() fetchProviders() @@ -524,14 +535,12 @@ onMounted(() => { .row-label { min-width: 140px; color: var(--text-secondary); font-size: 0.85rem; flex-shrink: 0; } .row-title { display: block; font-weight: 500; color: var(--text-primary); font-size: 0.9rem; } .row-desc { display: block; font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.15rem; } -.row-value { flex: 1; color: var(--text-primary); font-size: 0.9rem; } +.row-value { flex: 1; display: flex; align-items: center; justify-content: flex-end; } +.row-value .switch { margin-left: auto; } /* 内联输入框 */ -.inline-select, .inline-input { padding: 0.5rem 0.75rem; border: 1px solid var(--border-input); border-radius: 6px; background: var(--bg-input); color: var(--text-primary); font-size: 0.85rem; min-width: 180px; } +.inline-select, .inline-input { padding: 0.5rem 0.75rem; border: 1px solid var(--border-input); border-radius: 6px; background: var(--bg-input); color: var(--text-primary); font-size: 0.85rem; } .inline-input { width: 120px; } -.inline-input { width: 120px; } -.input-with-hint { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; } -.hint-inline { font-size: 0.75rem; color: var(--text-tertiary); } textarea { width: 100%; padding: 0.75rem; border: 1px solid var(--border-input); border-radius: 8px; background: var(--bg-input); color: var(--text-primary); font-size: 0.85rem; resize: vertical; min-height: 80px; box-sizing: border-box; } .hint-block { font-size: 0.75rem; color: var(--text-tertiary); margin-top: 0.5rem; } @@ -545,19 +554,19 @@ textarea { width: 100%; padding: 0.75rem; border: 1px solid var(--border-input); /* 列宽 */ .name-col { width: 15%; min-width: 120px; } -.info-col { width: 55%; min-width: 150px; } -.switch-col { text-align: center; width: 10%; } -.action-col { text-align: center; width: 10%; } -.ops-col { width: 10%; min-width: 180px; } +.info-col { width: 60%; min-width: 200px; } +.switch-col { text-align: center; width: 80px; } +.action-col { text-align: center; width: 80px; } +.ops-col { width: 15%; min-width: 180px; text-align: center; } /* Provider 单元格 */ .provider-name { font-weight: 600; font-size: 0.9rem; color: var(--text-primary); } .provider-badges { display: flex; gap: 0.35rem; margin-top: 0.35rem; } .badge { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 20px; font-size: 0.65rem; font-weight: 500; } -.badge.default { background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); color: white; } -.badge.enabled { background: rgba(52, 211, 153, 0.2); color: #16a34a; } -.badge.disabled { background: var(--bg-tertiary); color: var(--text-tertiary); } +.badge-default { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; } +.badge-enabled { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; } +.badge-disabled { background: var(--bg-tertiary); color: var(--text-tertiary); border: 1px solid var(--border-light); } .info-item { font-size: 0.8rem; color: var(--text-primary); word-break: break-all; } .info-item.sub { font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.2rem; } diff --git a/luxx/routes/providers.py b/luxx/routes/providers.py index aaa42f2..fddbb5f 100644 --- a/luxx/routes/providers.py +++ b/luxx/routes/providers.py @@ -30,6 +30,7 @@ class ProviderUpdate(BaseModel): base_url: Optional[str] = None api_key: Optional[str] = None default_model: Optional[str] = None + max_tokens: Optional[int] = None is_default: Optional[bool] = None enabled: Optional[bool] = None @@ -61,11 +62,6 @@ def create_provider( """Create a new LLM provider""" db = SessionLocal() try: - # If this is set as default, unset other defaults - if provider.is_default: - db.query(LLMProvider).filter( - LLMProvider.user_id == current_user.id - ).update({"is_default": False}) db_provider = LLMProvider( user_id=current_user.id,