diff --git a/frontend/src/pages/SystemSettings.tsx b/frontend/src/pages/SystemSettings.tsx index 4a035c3..d35af11 100644 --- a/frontend/src/pages/SystemSettings.tsx +++ b/frontend/src/pages/SystemSettings.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Palette, Monitor, Moon, Sun, Save, RotateCcw, CheckCircle2, Globe, Bell, Loader2, ExternalLink, ChevronDown, ChevronRight @@ -89,6 +89,12 @@ export default function SystemSettings() { const [notifications, setNotifications] = useState(defaultNotifications); const [llm, setLlm] = useState({ enabled: false, endpoint: '', model: '', apiKey: '' }); const [llmProvider, setLlmProvider] = useState('custom'); + // 先测试才能保存:null=未测 true=通过;任何表单修改都会重置 + const [llmTestOk, setLlmTestOk] = useState(null); + const updateLlm = (patch: Partial) => { + setLlm((prev) => ({ ...prev, ...patch })); + setLlmTestOk(null); + }; const [lark, setLark] = useState({ enabled: false, webhook: '', secret: '' }); const [ldap, setLdap] = useState({ enabled: false, @@ -109,9 +115,12 @@ export default function SystemSettings() { const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: apiClient.getSettings }); - // 用后端数据初始化表单(仅在数据变化时覆盖本地编辑态) + // 用后端数据初始化表单:仅在首次拿到数据时执行一次, + // 避免后续 settings 刷新(保存其它配置后 invalidate)把用户正在编辑的表单打回已保存值 + const initedRef = useRef(false); useEffect(() => { - if (!settings) return; + if (!settings || initedRef.current) return; + initedRef.current = true; setTheme(safeParse(settings.theme, defaultTheme)); setNotifications(safeParse(settings.notification, defaultNotifications)); setLlm((prev) => ({ @@ -238,6 +247,7 @@ export default function SystemSettings() { llmApiKey: llm.apiKey || undefined, }), onSuccess: (res) => { + setLlmTestOk(!!res.success); if (res.success) toast.success('AI 连接成功', { description: res.message }); else toast.error('AI 连接失败', { description: res.message }); }, @@ -534,7 +544,7 @@ export default function SystemSettings() {
@@ -547,7 +557,7 @@ export default function SystemSettings() { const preset = LLM_PROVIDERS.find((p) => p.id === id); if (preset && id !== 'custom') { // 非 custom:自动填入 endpoint/model(用户仍可修改) - setLlm((prev) => ({ ...prev, endpoint: preset.endpoint, model: preset.model })); + updateLlm({ endpoint: preset.endpoint, model: preset.model }); } }} className="w-full px-3 py-[6px] text-[12px] rounded-md border outline-none" @@ -562,7 +572,7 @@ export default function SystemSettings() {
- setLlm({ ...llm, endpoint: e.target.value })} placeholder="https://api.deepseek.com/v1" className="flex-1 min-w-0 px-3 py-[6px] text-[12px] rounded-md border outline-none font-mono" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }} /> + updateLlm({ endpoint: e.target.value })} placeholder="https://api.deepseek.com/v1" className="flex-1 min-w-0 px-3 py-[6px] text-[12px] rounded-md border outline-none font-mono" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }} /> {!!LLM_PROVIDERS.find((p) => p.id === llmProvider)?.keyUrl && ( p.id === llmProvider)!.keyUrl} @@ -579,22 +589,37 @@ export default function SystemSettings() {
- setLlm({ ...llm, model: e.target.value })} placeholder="deepseek-chat" className="w-full px-3 py-[6px] text-[12px] rounded-md border outline-none font-mono" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }} /> + updateLlm({ model: e.target.value })} placeholder="deepseek-chat" className="w-full px-3 py-[6px] text-[12px] rounded-md border outline-none font-mono" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }} />
- setLlm({ ...llm, apiKey: e.target.value })} placeholder={settings?.llmApiKeySet ? '已配置,留空不修改' : 'sk-...'} className="w-full px-3 py-[6px] text-[12px] rounded-md border outline-none font-mono" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }} /> + updateLlm({ apiKey: e.target.value })} placeholder={settings?.llmApiKeySet ? '已配置,留空不修改' : 'sk-...'} className="w-full px-3 py-[6px] text-[12px] rounded-md border outline-none font-mono" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }} />

未启用 AI 时,智能助手自动降级为本地规则模式(按服务名匹配,仍可正常发布)

- + {llm.enabled && llmTestOk === true && ( + ✓ 测试通过,可保存 + )} + {llm.enabled && llmTestOk === false && ( + 测试未通过,请检查配置 + )}
+ {llm.enabled && llmTestOk !== true && ( +

启用 AI 后需先「测试连接」通过才能保存,避免保存不可用配置

+ )}