feat: 项目重命名 SQL-Platform + 数据源表单重写(连接模式/流程/排除DB/脱敏)

- 项目名称 Web-Lite Pro → SQL-Platform 全局替换(13处)
- Instance 模型新增: access_mode, workflow, excluded_databases, mask_fields
- 创建数据源表单: 分区卡片(数据源信息/连接模式/流程/高级配置)
- 后端 JSON 字段序列化/反序列化
- 新增 5 个 Instance 字段测试 (133 total)
This commit is contained in:
logan committed 2026-05-22 22:08:05 +08:00
1 parent 1877522239
commit d5d19d6be4
16 files changed
+366 -73

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web-Lite Pro</title>
<title>SQL-Platform</title>
</head>
<body>
<div id="root"></div>
+2 -2
View File
@@ -57,7 +57,7 @@ export default function Layout() {
<Shield className="w-5 h-5 text-white" />
</div>
<span className="text-lg font-bold dg-gradient-text whitespace-nowrap">
Web-Lite
SQL-Platform
</span>
</div>
</div>
@@ -140,7 +140,7 @@ export default function Layout() {
{/* 顶部栏 */}
<header className="h-16 bg-[var(--dg-bg-secondary)] border-b border-[var(--dg-border)] flex items-center justify-between px-6 flex-shrink-0">
<div className="flex items-center gap-2 text-sm">
<span className="text-[var(--dg-text-muted)]">Web-Lite</span>
<span className="text-[var(--dg-text-muted)]">SQL-Platform</span>
<span className="text-[var(--dg-text-muted)]">/</span>
<span className="text-[var(--dg-text-primary)] font-medium">{getPageTitle()}</span>
</div>
+1 -1
View File
@@ -300,7 +300,7 @@ export const announcements: Announcement[] = [
},
{
id: 'ann-sql-1', title: '【规范】SQL 变更工单提交流程',
content: '1. 登录 Web-Lite 平台,进入「数据变更」页面\n2. 点击「新建工单」,填写标题和变更原因\n3. 选择目标数据库类型和实例\n4. 在 SQL 编辑器中输入变更语句\n5. 系统自动审核并评分,确认无误后提交\n6. 等待 DBA 审批通过后执行\n\n注意:生产环境变更必须提供回滚 SQL!',
content: '1. 登录 SQL-Platform 平台,进入「数据变更」页面\n2. 点击「新建工单」,填写标题和变更原因\n3. 选择目标数据库类型和实例\n4. 在 SQL 编辑器中输入变更语句\n5. 系统自动审核并评分,确认无误后提交\n6. 等待 DBA 审批通过后执行\n\n注意:生产环境变更必须提供回滚 SQL!',
priority: 'high', pinned: true, enabled: true, author: 'admin',
publishTime: '2024-01-10 08:00:00', expireTime: '2025-12-31 00:00:00',
readCount: 512, targetRoles: ['admin', 'dba', 'developer'],
+1 -1
View File
@@ -73,7 +73,7 @@ const initialAnnouncements: Announcement[] = [
readCount: 67, targetRoles: ['developer'],
},
{
id: 'ann-5', title: '【版本更新】Web-Lite v2.5.0 更新日志',
id: 'ann-5', title: '【版本更新】SQL-Platform v2.5.0 更新日志',
content: '本次更新内容:\n\n新增功能:\n- 支持多数据源统一管理\n- 查询审计新增敏感字段自动脱敏\n- 工单批量审批功能\n\n优化改进:\n- SQL 编辑器性能优化\n- 审批流程可视化升级\n- 通知渠道新增 Telegram 支持\n\nBug 修复:\n- 修复大结果集导出内存溢出问题\n- 修复 Redis 集群连接超时问题',
priority: 'low', pinned: false, enabled: false, author: 'admin',
publishTime: '2024-01-10 16:00:00', expireTime: '2024-02-10 00:00:00',
+156 -54
View File
@@ -3,7 +3,7 @@ import { useParams } from 'react-router-dom';
import {
Plus, RefreshCw, Search, Database, Server, Zap, Leaf,
Activity, HardDrive, ChevronRight, Trash2, Edit2,
XCircle,
XCircle, Eye, EyeOff, X,
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -293,6 +293,12 @@ function InstanceDetail({ instance, onBack, config, canEdit, canDelete, onDelete
}
/* ---------- CreateInstanceForm ---------- */
interface WorkflowStep {
stage: string;
role: string;
assignee: string;
}
function CreateInstanceForm({ defaultType, onCancel, onCreated }: { defaultType: string; onCancel: () => void; onCreated: () => void }) {
const [name, setName] = useState('');
const [dbType, setDbType] = useState(defaultType);
@@ -300,12 +306,32 @@ function CreateInstanceForm({ defaultType, onCancel, onCreated }: { defaultType:
const [port, setPort] = useState('3306');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showPwd, setShowPwd] = useState(false);
const [database, setDatabase] = useState('');
const [env, setEnv] = useState('development');
const [description, setDescription] = useState('');
const [accessMode, setAccessMode] = useState<'readwrite' | 'read' | 'write'>('readwrite');
const [excludedDbs, setExcludedDbs] = useState('');
const [maskFields, setMaskFields] = useState('');
const [workflow, setWorkflow] = useState<WorkflowStep[]>([
{ stage: '提交阶段', role: '提交人', assignee: '' },
{ stage: '开发负责人', role: '开发负责人', assignee: '' },
{ stage: '执行人', role: '执行人', assignee: '' },
]);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState('');
const updateWorkflow = (idx: number, field: keyof WorkflowStep, value: string) => {
setWorkflow(prev => prev.map((w, i) => i === idx ? { ...w, [field]: value } : w));
};
const addWorkflowStep = () => {
setWorkflow(prev => [...prev, { stage: '', role: '', assignee: '' }]);
};
const removeWorkflowStep = (idx: number) => {
setWorkflow(prev => prev.filter((_, i) => i !== idx));
};
const handleSubmit = async () => {
if (!name || !host || !port || !username || !password) { setError('请填写所有必填项'); return; }
setSubmitting(true);
@@ -314,7 +340,13 @@ function CreateInstanceForm({ defaultType, onCancel, onCreated }: { defaultType:
await instanceApi.create({
name, db_type: dbType, host, port: Number(port),
username, password, database: database || undefined,
env, description: description || undefined,
env, access_mode: accessMode,
workflow: workflow.filter(w => w.stage || w.assignee).length > 0 ? workflow.filter(w => w.stage || w.assignee) : undefined,
excluded_databases: excludedDbs ? excludedDbs.split(',').map(s => s.trim()).filter(Boolean) : undefined,
mask_fields: maskFields ? maskFields.split('\n').filter(Boolean).map(line => {
const [table = '', field = '', rule = 'mask'] = line.split(':');
return { table: table.trim(), field: field.trim(), rule: rule.trim() || 'mask' };
}) : undefined,
});
onCreated();
} catch (e: any) {
@@ -324,73 +356,143 @@ function CreateInstanceForm({ defaultType, onCancel, onCreated }: { defaultType:
}
};
const inputCls = 'w-full px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none';
const labelCls = 'block text-sm font-medium mb-1.5';
return (
<div className="space-y-5 max-w-3xl">
<div className="max-w-4xl mx-auto space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold">添加实例</h1>
<button onClick={onCancel} className="text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]"><XCircle className="w-6 h-6" /></button>
<h1 className="text-xl font-semibold">添加数据源</h1>
<button onClick={onCancel} className="text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)]"><XCircle className="w-5 h-5" /></button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">实例名称 <span className="text-red-400">*</span></label>
<input value={name} onChange={e => setName(e.target.value)} className="w-full px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" placeholder="例如:生产 MySQL 主库" />
</div>
{/* 数据源信息 */}
<Card className="dg-card">
<CardContent className="p-5 space-y-4">
<h3 className="text-sm font-semibold text-[var(--dg-text-secondary)]">数据源信息</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={labelCls}>环境 <span className="text-red-400">*</span></label>
<select value={env} onChange={e => setEnv(e.target.value)} className={inputCls}>
<option value="production">生产</option>
<option value="staging">预发</option>
<option value="development">开发</option>
</select>
</div>
<div>
<label className={labelCls}>名称 <span className="text-red-400">*</span></label>
<input value={name} onChange={e => setName(e.target.value)} className={inputCls} placeholder="pro-cex-rds-只读库" />
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<label className={labelCls}>地址 <span className="text-red-400">*</span></label>
<input value={host} onChange={e => setHost(e.target.value)} className={inputCls} placeholder="pro-rds-ro.51aipay.com" />
</div>
<div>
<label className={labelCls}>端口 <span className="text-red-400">*</span></label>
<input value={port} onChange={e => setPort(e.target.value)} className={inputCls} placeholder="3306" />
</div>
<div>
<label className={labelCls}>类型</label>
<select value={dbType} onChange={e => setDbType(e.target.value)} className={inputCls}>
<option value="mysql">MySQL</option><option value="postgres">PostgreSQL</option><option value="redis">Redis</option><option value="mongo">MongoDB</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className={labelCls}>用户名 <span className="text-red-400">*</span></label>
<input value={username} onChange={e => setUsername(e.target.value)} className={inputCls} placeholder="admin" />
</div>
<div>
<label className={labelCls}>密码 <span className="text-red-400">*</span></label>
<div className="relative">
<input type={showPwd ? 'text' : 'password'} value={password} onChange={e => setPassword(e.target.value)} className={inputCls + ' pr-10'} placeholder="数据库密码" />
<button type="button" onClick={() => setShowPwd(!showPwd)} className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]">
{showPwd ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
</div>
<div>
<label className={labelCls}>默认数据库</label>
<input value={database} onChange={e => setDatabase(e.target.value)} className={inputCls} placeholder="可选,留空则连接默认库" />
</div>
</CardContent>
</Card>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2">数据库类型 <span className="text-red-400">*</span></label>
<select value={dbType} onChange={e => setDbType(e.target.value)} className="w-full px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none">
<option value="mysql">MySQL</option><option value="postgres">PostgreSQL</option><option value="redis">Redis</option><option value="mongo">MongoDB</option>
</select>
{/* 连接模式 */}
<Card className="dg-card">
<CardContent className="p-5 space-y-4">
<h3 className="text-sm font-semibold text-[var(--dg-text-secondary)]">连接模式</h3>
<div className="flex gap-3">
{[
{ value: 'readwrite' as const, label: '读写' },
{ value: 'write' as const, label: '写' },
{ value: 'read' as const, label: '读' },
].map(m => (
<button
key={m.value}
onClick={() => setAccessMode(m.value)}
className={`px-5 py-2 rounded-lg text-sm font-medium transition-all ${
accessMode === m.value
? 'bg-blue-500/15 text-blue-400 border border-blue-500/30'
: 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)] border border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]'
}`}
>
{m.label}
</button>
))}
</div>
<div>
<label className="block text-sm font-medium mb-2">环境 <span className="text-red-400">*</span></label>
<select value={env} onChange={e => setEnv(e.target.value)} className="w-full px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none">
<option value="development">开发</option><option value="staging">预发</option><option value="production">生产</option>
</select>
</div>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium mb-2">主机地址 <span className="text-red-400">*</span></label>
<input value={host} onChange={e => setHost(e.target.value)} className="w-full px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" placeholder="192.168.1.100" />
{/* 流程配置 */}
<Card className="dg-card">
<CardContent className="p-5 space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-[var(--dg-text-secondary)]">流程</h3>
<button onClick={addWorkflowStep} className="text-xs text-blue-400 hover:text-blue-300">+ 添加阶段</button>
</div>
<div>
<label className="block text-sm font-medium mb-2">端口 <span className="text-red-400">*</span></label>
<input value={port} onChange={e => setPort(e.target.value)} className="w-full px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" placeholder="3306" />
<div className="space-y-3">
{workflow.map((step, idx) => (
<div key={idx} className="flex items-center gap-3">
<span className="text-xs text-[var(--dg-text-muted)] w-6 text-right">{idx + 1}</span>
<input value={step.stage} onChange={e => updateWorkflow(idx, 'stage', e.target.value)} className={inputCls + ' flex-1'} placeholder="提交阶段" />
<input value={step.role} onChange={e => updateWorkflow(idx, 'role', e.target.value)} className={inputCls + ' w-32'} placeholder="角色" />
<input value={step.assignee} onChange={e => updateWorkflow(idx, 'assignee', e.target.value)} className={inputCls + ' flex-1'} placeholder="负责人" />
{workflow.length > 1 && (
<button onClick={() => removeWorkflowStep(idx)} className="text-[var(--dg-text-muted)] hover:text-red-400"><X className="w-4 h-4" /></button>
)}
</div>
))}
</div>
<div>
<label className="block text-sm font-medium mb-2">默认数据库</label>
<input value={database} onChange={e => setDatabase(e.target.value)} className="w-full px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" placeholder="可选" />
</div>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-2 gap-4">
{/* 排除的数据库 & 脱敏字段 */}
<Card className="dg-card">
<CardContent className="p-5 space-y-4">
<h3 className="text-sm font-semibold text-[var(--dg-text-secondary)]">高级配置</h3>
<div>
<label className="block text-sm font-medium mb-2">用户名 <span className="text-red-400">*</span></label>
<input value={username} onChange={e => setUsername(e.target.value)} className="w-full px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" placeholder="root" />
<label className={labelCls}>排除的数据库</label>
<input value={excludedDbs} onChange={e => setExcludedDbs(e.target.value)} className={inputCls} placeholder="以逗号分隔,如:sys, information_schema, mysql" />
</div>
<div>
<label className="block text-sm font-medium mb-2">密码 <span className="text-red-400">*</span></label>
<input type="password" value={password} onChange={e => setPassword(e.target.value)} className="w-full px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" placeholder="数据库密码" />
<label className={labelCls}>脱敏字段</label>
<textarea value={maskFields} onChange={e => setMaskFields(e.target.value)} className={inputCls + ' min-h-[80px] resize-y'} placeholder={'每行一个,格式:表名:字段名:规则\n例如:\nusers:phone:mask\nusers:id_card:mask\norders:amount:hide'} />
</div>
</div>
</CardContent>
</Card>
<div>
<label className="block text-sm font-medium mb-2">描述</label>
<textarea value={description} onChange={e => setDescription(e.target.value)} className="w-full px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm min-h-[80px] resize-y focus:border-blue-400 focus:outline-none" placeholder="可选描述" />
</div>
{error && <div className="text-sm text-red-400">{error}</div>}
{error && <div className="text-sm text-red-400">{error}</div>}
<div className="flex justify-end gap-3 pt-4">
<button onClick={onCancel} className="px-5 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm font-medium hover:bg-[var(--dg-bg-hover)]">取消</button>
<button onClick={handleSubmit} disabled={submitting} className="px-5 py-2.5 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg text-sm font-medium text-white hover:opacity-90 disabled:opacity-50">
{submitting ? '创建中...' : '添加实例'}
</button>
</div>
<div className="flex justify-end gap-3 pb-6">
<button onClick={onCancel} className="px-5 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm font-medium hover:bg-[var(--dg-bg-hover)]">取消</button>
<button onClick={handleSubmit} disabled={submitting} className="px-5 py-2.5 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg text-sm font-medium text-white hover:opacity-90 disabled:opacity-50">
{submitting ? '创建中...' : '添加数据源'}
</button>
</div>
</div>
);
+1 -1
View File
@@ -59,7 +59,7 @@ export default function LoginPage() {
<Shield className="w-7 h-7 text-white" />
</div>
<h1 className="text-5xl font-bold dg-gradient-text">
Web-Lite Pro
SQL-Platform
</h1>
</div>
<p className="text-lg text-[var(--dg-text-secondary)] leading-relaxed">