import { useEffect, useState } from 'react'; import { Palette, Monitor, Moon, Sun, Save, RotateCcw, CheckCircle2, Globe, Bell, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { apiClient } from '@/lib/api'; interface ThemeConfig { mode: 'light' | 'dark' | 'system'; primaryColor: string; fontSize: 'small' | 'medium' | 'large'; density: 'compact' | 'comfortable' | 'spacious'; sidebarStyle: 'collapsed' | 'icons' | 'full'; borderRadius: 'sharp' | 'soft' | 'round'; } interface NotificationConfig { email: boolean; browser: boolean; pipelineSuccess: boolean; pipelineFailed: boolean; securityAlert: boolean; } const defaultTheme: ThemeConfig = { mode: 'light', primaryColor: '#0969da', fontSize: 'medium', density: 'comfortable', sidebarStyle: 'full', borderRadius: 'soft', }; const defaultNotifications: NotificationConfig = { email: true, browser: true, pipelineSuccess: false, pipelineFailed: true, securityAlert: true, }; // 后端 theme / notification 均以 JSON 字符串存储,安全解析并合并默认值 function safeParse(raw: unknown, fallback: T): T { if (typeof raw !== 'string' || !raw) return fallback; try { const parsed = JSON.parse(raw); if (parsed && typeof parsed === 'object') return { ...fallback, ...parsed }; return fallback; } catch { return fallback; } } const colorOptions = [ { name: '蓝色', value: '#0969da', class: 'bg-[#0969da]' }, { name: '绿色', value: '#1a7f37', class: 'bg-[#1a7f37]' }, { name: '紫色', value: '#8250df', class: 'bg-[#8250df]' }, { name: '橙色', value: '#bc4c00', class: 'bg-[#bc4c00]' }, { name: '红色', value: '#cf222e', class: 'bg-[#cf222e]' }, { name: '青色', value: '#0e7c7b', class: 'bg-[#0e7c7b]' }, ]; export default function SystemSettings() { const queryClient = useQueryClient(); const [theme, setTheme] = useState(defaultTheme); const [notifications, setNotifications] = useState(defaultNotifications); const [llm, setLlm] = useState({ enabled: false, endpoint: '', model: '', apiKey: '' }); const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: apiClient.getSettings }); // 用后端数据初始化表单(仅在数据变化时覆盖本地编辑态) useEffect(() => { if (!settings) return; setTheme(safeParse(settings.theme, defaultTheme)); setNotifications(safeParse(settings.notification, defaultNotifications)); setLlm((prev) => ({ ...prev, enabled: !!settings.llmEnabled, endpoint: settings.llmEndpoint || '', model: settings.llmModel || '', apiKey: '', // 只写:回显永远留空 })); }, [settings]); const saveLlmMutation = useMutation({ mutationFn: () => { const body: any = { llmEnabled: llm.enabled, llmEndpoint: llm.endpoint, llmModel: llm.model }; if (llm.apiKey) body.llmApiKey = llm.apiKey; return apiClient.updateSettings(body); }, onSuccess: () => { toast.success('AI 配置已保存'); setLlm((p) => ({ ...p, apiKey: '' })); queryClient.invalidateQueries({ queryKey: ['settings'] }); }, onError: (err: Error) => toast.error('保存失败', { description: err.message }), }); const testLlmMutation = useMutation({ mutationFn: apiClient.testLLM, onSuccess: (res) => { if (res.success) toast.success('AI 连接成功', { description: res.message }); else toast.error('AI 连接失败', { description: res.message }); }, onError: (err: Error) => toast.error('测试失败', { description: err.message }), }); const saveMutation = useMutation({ mutationFn: () => apiClient.updateSettings({ theme: JSON.stringify(theme), notification: JSON.stringify(notifications), }), onSuccess: () => { toast.success('设置已保存', { description: '主题和通知设置已更新' }); queryClient.invalidateQueries({ queryKey: ['settings'] }); }, onError: (err: Error) => toast.error(err.message), }); const handleReset = () => { setTheme(defaultTheme); setNotifications(defaultNotifications); toast.info('已恢复默认设置', { description: '仅本地重置,点击「保存设置」后生效' }); }; return (
{/* Header */}

系统设置

{!settings && ( 加载中 )}
{/* Content */}
{/* Appearance */}
外观主题
{/* Mode */}
{[ { id: 'light', label: '浅色', icon: Sun }, { id: 'dark', label: '深色', icon: Moon }, { id: 'system', label: '跟随系统', icon: Monitor }, ].map(m => ( ))}
{/* Primary color */}
{colorOptions.map(c => ( ))}
{/* Font size */}
{[ { id: 'small', label: '小', size: '12px' }, { id: 'medium', label: '中', size: '14px' }, { id: 'large', label: '大', size: '16px' }, ].map(f => ( ))}
{/* Density */}
{[ { id: 'compact', label: '紧凑' }, { id: 'comfortable', label: '舒适' }, { id: 'spacious', label: '宽松' }, ].map(d => ( ))}
{/* Border radius */}
{[ { id: 'sharp', label: '直角', radius: '0px' }, { id: 'soft', label: '圆角', radius: '6px' }, { id: 'round', label: '大圆角', radius: '12px' }, ].map(r => ( ))}
{/* Notifications */}
通知设置
{[ { key: 'email', label: '邮件通知', desc: '通过邮件接收重要通知' }, { key: 'browser', label: '浏览器通知', desc: '在浏览器中显示推送通知' }, { key: 'pipelineSuccess', label: '流水线成功通知', desc: '流水线执行成功时发送通知' }, { key: 'pipelineFailed', label: '流水线失败通知', desc: '流水线执行失败时发送通知(推荐开启)' }, { key: 'securityAlert', label: '安全警报', desc: '凭证泄露、异常登录等安全事件' }, ].map(item => ( ))}
{/* Language & Region */}
语言与区域

语言与时区暂未生效,仅作展示

{/* AI 助手配置 */}
AI 助手(智能发布) {settings?.llmEnabled && settings?.llmApiKeySet ? '已启用' : '未启用(规则模式)'}
setLlm({ ...llm, endpoint: e.target.value })} placeholder="https://api.deepseek.com/v1" 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, 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)' }} />

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

); }