feat: FlowStack 发布系统首个可运行版本 (gin + React)
- Go gin 后端:流水线/节点/连线/运行/日志/GitOps/插件 REST API, SQLite(GORM) 自动建表 + seed,模拟执行引擎 + SSE 实时日志推送, 前端产物 go:embed 嵌入单二进制,一个端口同时服务 API 与页面 - React 19 前端:画布式流水线编辑器(React Flow + Tekton 任务库)、 GitOps 部署、批量发布、发布记录等 14 个模块页面, tRPC 全部替换为 REST(TanStack Query) - 首次启动生成随机密码管理员账号(日志 + data/initial-admin-password.txt) - run.sh 一键构建启动(默认端口 9527,支持 start/stop/restart/build/dev)
This commit is contained in:
commit
04d1342b27
115 files changed
+23268
No files matched your search
@@ -0,0 +1,241 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Palette, Monitor, Moon, Sun,
|
||||
Save, RotateCcw, CheckCircle2, Globe, Bell
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface ThemeConfig {
|
||||
mode: 'light' | 'dark' | 'system';
|
||||
primaryColor: string;
|
||||
fontSize: 'small' | 'medium' | 'large';
|
||||
density: 'compact' | 'comfortable' | 'spacious';
|
||||
sidebarStyle: 'collapsed' | 'icons' | 'full';
|
||||
borderRadius: 'sharp' | 'soft' | 'round';
|
||||
}
|
||||
|
||||
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 [theme, setTheme] = useState<ThemeConfig>({
|
||||
mode: 'light',
|
||||
primaryColor: '#0969da',
|
||||
fontSize: 'medium',
|
||||
density: 'comfortable',
|
||||
sidebarStyle: 'full',
|
||||
borderRadius: 'soft',
|
||||
});
|
||||
|
||||
const [notifications, setNotifications] = useState({
|
||||
email: true,
|
||||
browser: true,
|
||||
pipelineSuccess: false,
|
||||
pipelineFailed: true,
|
||||
securityAlert: true,
|
||||
});
|
||||
|
||||
const handleSave = () => {
|
||||
toast.success('设置已保存', { description: '主题和偏好设置已更新' });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setTheme({
|
||||
mode: 'light',
|
||||
primaryColor: '#0969da',
|
||||
fontSize: 'medium',
|
||||
density: 'comfortable',
|
||||
sidebarStyle: 'full',
|
||||
borderRadius: 'soft',
|
||||
});
|
||||
toast.info('已恢复默认设置');
|
||||
};
|
||||
|
||||
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-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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-5">
|
||||
<div className="max-w-3xl mx-auto space-y-4">
|
||||
{/* Appearance */}
|
||||
<div className="rounded-lg border overflow-hidden" style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}>
|
||||
<div className="px-4 py-2.5 border-b flex items-center gap-2" style={{ borderColor: 'var(--border-color)', background: 'var(--panel-bg-secondary)' }}>
|
||||
<Palette className="w-4 h-4" style={{ color: 'var(--accent-blue)' }} />
|
||||
<span className="text-[13px] font-semibold" style={{ color: 'var(--text-primary)' }}>外观主题</span>
|
||||
</div>
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Mode */}
|
||||
<div>
|
||||
<label className="block text-[12px] font-medium mb-2" style={{ color: 'var(--text-secondary)' }}>主题模式</label>
|
||||
<div className="flex items-center gap-2">
|
||||
{[
|
||||
{ id: 'light', label: '浅色', icon: Sun },
|
||||
{ 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 }))}
|
||||
className="flex items-center gap-2 px-4 py-[7px] text-[12px] font-medium rounded-md border transition-colors"
|
||||
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)',
|
||||
}}>
|
||||
<m.icon className="w-3.5 h-3.5" />{m.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Primary color */}
|
||||
<div>
|
||||
<label className="block text-[12px] font-medium mb-2" style={{ color: 'var(--text-secondary)' }}>主题色</label>
|
||||
<div className="flex items-center gap-2">
|
||||
{colorOptions.map(c => (
|
||||
<button key={c.value} onClick={() => setTheme(p => ({ ...p, primaryColor: c.value }))}
|
||||
className={`w-8 h-8 rounded-full ${c.class} flex items-center justify-center transition-transform hover:scale-110`}
|
||||
title={c.name}>
|
||||
{theme.primaryColor === c.value && <CheckCircle2 className="w-4 h-4 text-white" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Font size */}
|
||||
<div>
|
||||
<label className="block text-[12px] font-medium mb-2" style={{ color: 'var(--text-secondary)' }}>字体大小</label>
|
||||
<div className="flex items-center gap-2">
|
||||
{[
|
||||
{ id: 'small', label: '小', size: '12px' },
|
||||
{ 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 }))}
|
||||
className="flex-1 py-[7px] text-[12px] font-medium rounded-md border transition-colors text-center"
|
||||
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)',
|
||||
fontSize: f.size,
|
||||
}}>
|
||||
{f.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Density */}
|
||||
<div>
|
||||
<label className="block text-[12px] font-medium mb-2" style={{ color: 'var(--text-secondary)' }}>界面密度</label>
|
||||
<div className="flex items-center gap-2">
|
||||
{[
|
||||
{ id: 'compact', label: '紧凑' },
|
||||
{ id: 'comfortable', label: '舒适' },
|
||||
{ id: 'spacious', label: '宽松' },
|
||||
].map(d => (
|
||||
<button key={d.id} onClick={() => setTheme(p => ({ ...p, density: d.id as any }))}
|
||||
className="flex-1 py-[7px] text-[12px] font-medium rounded-md border transition-colors"
|
||||
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)',
|
||||
}}>
|
||||
{d.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Border radius */}
|
||||
<div>
|
||||
<label className="block text-[12px] font-medium mb-2" style={{ color: 'var(--text-secondary)' }}>圆角风格</label>
|
||||
<div className="flex items-center gap-2">
|
||||
{[
|
||||
{ id: 'sharp', label: '直角', radius: '0px' },
|
||||
{ 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 }))}
|
||||
className="flex-1 py-[7px] text-[12px] font-medium border transition-colors"
|
||||
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)',
|
||||
borderRadius: r.radius,
|
||||
}}>
|
||||
{r.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notifications */}
|
||||
<div className="rounded-lg border overflow-hidden" style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}>
|
||||
<div className="px-4 py-2.5 border-b flex items-center gap-2" style={{ borderColor: 'var(--border-color)', background: 'var(--panel-bg-secondary)' }}>
|
||||
<Bell className="w-4 h-4" style={{ color: 'var(--accent-blue)' }} />
|
||||
<span className="text-[13px] font-semibold" style={{ color: 'var(--text-primary)' }}>通知设置</span>
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
{[
|
||||
{ key: 'email', label: '邮件通知', desc: '通过邮件接收重要通知' },
|
||||
{ key: 'browser', label: '浏览器通知', desc: '在浏览器中显示推送通知' },
|
||||
{ key: 'pipelineSuccess', label: '流水线成功通知', desc: '流水线执行成功时发送通知' },
|
||||
{ key: 'pipelineFailed', label: '流水线失败通知', desc: '流水线执行失败时发送通知(推荐开启)' },
|
||||
{ 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]}
|
||||
onChange={e => setNotifications(p => ({ ...p, [item.key]: e.target.checked }))}
|
||||
className="mt-0.5 w-4 h-4" style={{ accentColor: 'var(--accent-blue)' }} />
|
||||
<div>
|
||||
<div className="text-[13px] font-medium" style={{ color: 'var(--text-primary)' }}>{item.label}</div>
|
||||
<div className="text-[11px]" style={{ color: 'var(--text-tertiary)' }}>{item.desc}</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Language & Region */}
|
||||
<div className="rounded-lg border overflow-hidden" style={{ background: 'var(--panel-bg)', borderColor: 'var(--border-color)' }}>
|
||||
<div className="px-4 py-2.5 border-b flex items-center gap-2" style={{ borderColor: 'var(--border-color)', background: 'var(--panel-bg-secondary)' }}>
|
||||
<Globe className="w-4 h-4" style={{ color: 'var(--accent-blue)' }} />
|
||||
<span className="text-[13px] font-semibold" style={{ color: 'var(--text-primary)' }}>语言与区域</span>
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div><label className="block text-[12px] font-medium mb-1" style={{ color: 'var(--text-secondary)' }}>界面语言</label>
|
||||
<select className="w-full px-3 py-[6px] text-[13px] rounded-md border outline-none" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }}>
|
||||
<option>简体中文</option><option>English</option><option>日本語</option>
|
||||
</select></div>
|
||||
<div><label className="block text-[12px] font-medium mb-1" style={{ color: 'var(--text-secondary)' }}>时区</label>
|
||||
<select className="w-full px-3 py-[6px] text-[13px] rounded-md border outline-none" style={{ background: 'var(--panel-bg-secondary)', borderColor: 'var(--border-color-light)', color: 'var(--text-primary)' }}>
|
||||
<option>Asia/Shanghai (UTC+8)</option><option>UTC</option><option>America/New_York</option>
|
||||
</select></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user