feat: 去 mock 化 —— 全模块接入真实数据 (参考 jianmu/opsx-jenkins/aiops)
后端(35+ 新路由): - AES-256-GCM 字段加密(enc:v1:),密钥自动生成 data/encryption.key - 新模型:Host/K8sCluster/Credential/BuildNode/DeployTool/WebhookEndpoint/ JenkinsServer/SystemSetting;Pipeline 扩展触发器/变量/缓存列, PipelineRun 扩展 buildNumber/externalUrl - 资源 CRUD + 真实测试端点:主机/集群/构建节点 TCP 拨号、 工具真实执行版本检测(保守安装策略)、Webhook HMAC-SHA256 签名发送、 入站流水线触发 /api/hooks/pipeline/:id - Jenkins 完整集成(移植 opsx-jenkins 生产客户端):crumb/BasicAuth/ buildWithParameters/Location 队列/progressiveText 增量控制台, 服务器 CRUD+测试、Job 树拉取(folder 压平)、导入为流水线、 jenkins 类型流水线真实执行回传日志 - 流水线列表附带 lastRun、运行重试、summary 统计、系统设置单行表 - 运行结束触发出站 Webhook 通知;GitOps 同步状态真实推进 前端(14 页全部去 mock): - 流水线列表/发布记录/批量发布/环境/凭证/构建节点/工具/Webhook/ 系统设置/Jenkins 导入/GitOps(资源树+差异) 全部接入真实 API - 编辑器 4 Tab 持久化(基本信息/触发设置含真实 Webhook URL/变量缓存/发布历史) - TopBar 真实流水线切换 + 仅保存/保存并运行 - Sidebar 徽标真实统计;删除未使用组件与 mock 数据文件
This commit is contained in:
1 parent
04d1342b27
commit
26341c3e8a
37 files changed
+5702
-4683
No files matched your search
@@ -1,9 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Palette, Monitor, Moon, Sun,
|
||||
Save, RotateCcw, CheckCircle2, Globe, Bell
|
||||
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';
|
||||
@@ -14,6 +16,43 @@ interface ThemeConfig {
|
||||
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<T extends object>(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]' },
|
||||
@@ -24,50 +63,57 @@ const colorOptions = [
|
||||
];
|
||||
|
||||
export default function SystemSettings() {
|
||||
const [theme, setTheme] = useState<ThemeConfig>({
|
||||
mode: 'light',
|
||||
primaryColor: '#0969da',
|
||||
fontSize: 'medium',
|
||||
density: 'comfortable',
|
||||
sidebarStyle: 'full',
|
||||
borderRadius: 'soft',
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
const [theme, setTheme] = useState<ThemeConfig>(defaultTheme);
|
||||
const [notifications, setNotifications] = useState<NotificationConfig>(defaultNotifications);
|
||||
|
||||
const [notifications, setNotifications] = useState({
|
||||
email: true,
|
||||
browser: true,
|
||||
pipelineSuccess: false,
|
||||
pipelineFailed: true,
|
||||
securityAlert: true,
|
||||
});
|
||||
const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: apiClient.getSettings });
|
||||
|
||||
const handleSave = () => {
|
||||
toast.success('设置已保存', { description: '主题和偏好设置已更新' });
|
||||
};
|
||||
// 用后端数据初始化表单(仅在数据变化时覆盖本地编辑态)
|
||||
useEffect(() => {
|
||||
if (!settings) return;
|
||||
setTheme(safeParse<ThemeConfig>(settings.theme, defaultTheme));
|
||||
setNotifications(safeParse<NotificationConfig>(settings.notification, defaultNotifications));
|
||||
}, [settings]);
|
||||
|
||||
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({
|
||||
mode: 'light',
|
||||
primaryColor: '#0969da',
|
||||
fontSize: 'medium',
|
||||
density: 'comfortable',
|
||||
sidebarStyle: 'full',
|
||||
borderRadius: 'soft',
|
||||
});
|
||||
toast.info('已恢复默认设置');
|
||||
setTheme(defaultTheme);
|
||||
setNotifications(defaultNotifications);
|
||||
toast.info('已恢复默认设置', { description: '仅本地重置,点击「保存设置」后生效' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col" style={{ background: 'var(--canvas-bg)' }}>
|
||||
{/* Header */}
|
||||
<div className="px-5 py-3 border-b flex items-center justify-between flex-shrink-0" style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}>
|
||||
<h1 className="text-[15px] font-semibold" style={{ color: 'var(--text-primary)' }}>系统设置</h1>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-[15px] font-semibold" style={{ color: 'var(--text-primary)' }}>系统设置</h1>
|
||||
{!settings && (
|
||||
<span className="flex items-center gap-1 text-[11px]" style={{ color: 'var(--text-tertiary)' }}>
|
||||
<Loader2 className="w-3 h-3 animate-spin" />加载中
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button onClick={handleReset} className="flex items-center gap-1 px-3 py-[5px] text-[12px] font-medium rounded-md border" style={{ borderColor: 'var(--border-color-light)', color: 'var(--text-secondary)' }}>
|
||||
<RotateCcw className="w-3.5 h-3.5" />恢复默认
|
||||
</button>
|
||||
<button onClick={handleSave} className="flex items-center gap-1 px-3 py-[5px] text-[12px] font-medium rounded-md text-white" style={{ background: 'var(--accent-blue)' }}>
|
||||
<Save className="w-3.5 h-3.5" />保存设置
|
||||
<button onClick={() => saveMutation.mutate()} disabled={saveMutation.isPending}
|
||||
className="flex items-center gap-1 px-3 py-[5px] text-[12px] font-medium rounded-md text-white disabled:opacity-60" style={{ background: 'var(--accent-blue)' }}>
|
||||
{saveMutation.isPending ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Save className="w-3.5 h-3.5" />}
|
||||
{saveMutation.isPending ? '保存中...' : '保存设置'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,9 +137,9 @@ export default function SystemSettings() {
|
||||
{ id: 'dark', label: '深色', icon: Moon },
|
||||
{ id: 'system', label: '跟随系统', icon: Monitor },
|
||||
].map(m => (
|
||||
<button key={m.id} onClick={() => setTheme(p => ({ ...p, mode: m.id as any }))}
|
||||
<button key={m.id} onClick={() => setTheme(p => ({ ...p, mode: m.id as ThemeConfig['mode'] }))}
|
||||
className="flex items-center gap-2 px-4 py-[7px] text-[12px] font-medium rounded-md border transition-colors"
|
||||
style={{
|
||||
style={{
|
||||
borderColor: theme.mode === m.id ? 'var(--accent-blue)' : 'var(--border-color-light)',
|
||||
background: theme.mode === m.id ? 'var(--accent-blue-light)' : 'transparent',
|
||||
color: theme.mode === m.id ? 'var(--accent-blue)' : 'var(--text-secondary)',
|
||||
@@ -127,9 +173,9 @@ export default function SystemSettings() {
|
||||
{ id: 'medium', label: '中', size: '14px' },
|
||||
{ id: 'large', label: '大', size: '16px' },
|
||||
].map(f => (
|
||||
<button key={f.id} onClick={() => setTheme(p => ({ ...p, fontSize: f.id as any }))}
|
||||
<button key={f.id} onClick={() => setTheme(p => ({ ...p, fontSize: f.id as ThemeConfig['fontSize'] }))}
|
||||
className="flex-1 py-[7px] text-[12px] font-medium rounded-md border transition-colors text-center"
|
||||
style={{
|
||||
style={{
|
||||
borderColor: theme.fontSize === f.id ? 'var(--accent-blue)' : 'var(--border-color-light)',
|
||||
background: theme.fontSize === f.id ? 'var(--accent-blue-light)' : 'transparent',
|
||||
color: theme.fontSize === f.id ? 'var(--accent-blue)' : 'var(--text-secondary)',
|
||||
@@ -150,9 +196,9 @@ export default function SystemSettings() {
|
||||
{ id: 'comfortable', label: '舒适' },
|
||||
{ id: 'spacious', label: '宽松' },
|
||||
].map(d => (
|
||||
<button key={d.id} onClick={() => setTheme(p => ({ ...p, density: d.id as any }))}
|
||||
<button key={d.id} onClick={() => setTheme(p => ({ ...p, density: d.id as ThemeConfig['density'] }))}
|
||||
className="flex-1 py-[7px] text-[12px] font-medium rounded-md border transition-colors"
|
||||
style={{
|
||||
style={{
|
||||
borderColor: theme.density === d.id ? 'var(--accent-blue)' : 'var(--border-color-light)',
|
||||
background: theme.density === d.id ? 'var(--accent-blue-light)' : 'transparent',
|
||||
color: theme.density === d.id ? 'var(--accent-blue)' : 'var(--text-secondary)',
|
||||
@@ -172,9 +218,9 @@ export default function SystemSettings() {
|
||||
{ id: 'soft', label: '圆角', radius: '6px' },
|
||||
{ id: 'round', label: '大圆角', radius: '12px' },
|
||||
].map(r => (
|
||||
<button key={r.id} onClick={() => setTheme(p => ({ ...p, borderRadius: r.id as any }))}
|
||||
<button key={r.id} onClick={() => setTheme(p => ({ ...p, borderRadius: r.id as ThemeConfig['borderRadius'] }))}
|
||||
className="flex-1 py-[7px] text-[12px] font-medium border transition-colors"
|
||||
style={{
|
||||
style={{
|
||||
borderColor: theme.borderRadius === r.id ? 'var(--accent-blue)' : 'var(--border-color-light)',
|
||||
background: theme.borderRadius === r.id ? 'var(--accent-blue-light)' : 'transparent',
|
||||
color: theme.borderRadius === r.id ? 'var(--accent-blue)' : 'var(--text-secondary)',
|
||||
@@ -203,7 +249,7 @@ export default function SystemSettings() {
|
||||
{ key: 'securityAlert', label: '安全警报', desc: '凭证泄露、异常登录等安全事件' },
|
||||
].map(item => (
|
||||
<label key={item.key} className="flex items-start gap-3 cursor-pointer">
|
||||
<input type="checkbox" checked={(notifications as any)[item.key]}
|
||||
<input type="checkbox" checked={notifications[item.key as keyof NotificationConfig]}
|
||||
onChange={e => setNotifications(p => ({ ...p, [item.key]: e.target.checked }))}
|
||||
className="mt-0.5 w-4 h-4" style={{ accentColor: 'var(--accent-blue)' }} />
|
||||
<div>
|
||||
@@ -232,6 +278,7 @@ export default function SystemSettings() {
|
||||
<option>Asia/Shanghai (UTC+8)</option><option>UTC</option><option>America/New_York</option>
|
||||
</select></div>
|
||||
</div>
|
||||
<p className="text-[11px]" style={{ color: 'var(--text-tertiary)' }}>语言与时区暂未生效,仅作展示</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user