feat: Phase 2 — frontend-backend integration complete

- All 5 core pages now use real API instead of mock data
  - Dashboard: /api/v1/dashboard/stats
  - Tickets: list/create/detail/approve/execute/statistics
  - Users: list/create/toggle-active/delete
  - Instances: list/create/test-connection/delete
  - Queries: execute with result display + sensitive field masking
- Updated DashboardStats type to match backend schema
- Fixed InstancePage to work with InstanceResponse (no qps/cpu/metrics yet)
- Added create forms for Tickets, Users, Instances
- Added connection test in InstanceDetail
- Updated TDD_PLAN.md: Phase 1 ✅ (44 tests), Phase 2 ✅
- Updated ROADMAP.md: v0.2.0 ✅
- Updated PROJECT_EVALUATION.md: score 5.7 → 6.2
- Frontend build passes with zero TS errors
- 44 backend tests still passing
This commit is contained in:
logan committed 2026-05-22 12:39:48 +08:00
1 parent c0c731acfc
commit 27989274f5
10 files changed
+1074 -840

No files matched your search

+263 -168
View File
@@ -1,16 +1,31 @@
import { useState, useMemo } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'react-router-dom';
import {
Plus, RefreshCw, Search, Database, Server, Zap, Leaf,
Activity, HardDrive, Cpu, MemoryStick, Clock, ChevronRight, Trash2, Edit2,
Undo2,
Activity, HardDrive, ChevronRight, Trash2, Edit2,
XCircle,
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { dbInstances } from '@/lib/mock';
import { useActionPermission } from '@/hooks/useAuth';
import PageHeader from '@/components/PageHeader';
import EmptyState from '@/components/EmptyState';
import type { DBInstance } from '@/types';
import { instanceApi } from '@/api/instance';
interface InstanceItem {
id: number;
name: string;
db_type: string;
host: string;
port: number;
username: string;
database: string | null;
env: string;
description: string | null;
status: string;
version: string | null;
created_at: string;
}
const dbTypeConfig: Record<string, { icon: React.ElementType; color: string; bg: string; label: string }> = {
mysql: { icon: Database, color: 'text-[#26a69a]', bg: 'bg-[rgba(0,117,143,0.15)]', label: 'MySQL' },
@@ -19,6 +34,10 @@ const dbTypeConfig: Record<string, { icon: React.ElementType; color: string; bg:
mongo: { icon: Leaf, color: 'text-[#66bb6a]', bg: 'bg-[rgba(71,162,72,0.15)]', label: 'MongoDB' },
};
const envLabel: Record<string, string> = {
production: '生产', staging: '预发', development: '开发',
};
export default function InstancePage() {
const { type = 'mysql' } = useParams<{ type: string }>();
const canAdd = useActionPermission('instance.add');
@@ -28,54 +47,75 @@ export default function InstancePage() {
const [searchQuery, setSearchQuery] = useState('');
const [filterStatus, setFilterStatus] = useState('');
const [filterEnv, setFilterEnv] = useState('');
const [selectedInstance, setSelectedInstance] = useState<DBInstance | null>(null);
const [selectedInstance, setSelectedInstance] = useState<InstanceItem | null>(null);
const [view, setView] = useState<'grid' | 'detail'>('grid');
const [instances, setInstances] = useState<InstanceItem[]>([]);
const [loading, setLoading] = useState(false);
const [showCreate, setShowCreate] = useState(false);
const config: { icon: React.ElementType; color: string; bg: string; label: string } = dbTypeConfig[type] || dbTypeConfig.mysql;
const config = dbTypeConfig[type] || dbTypeConfig.mysql;
const instances = useMemo(() => {
return dbInstances.filter(i => {
if (i.dbType !== type) return false;
if (filterStatus && i.status !== filterStatus) return false;
if (filterEnv && i.env !== filterEnv) return false;
if (searchQuery && !i.name.includes(searchQuery) && !i.host.includes(searchQuery)) return false;
return true;
});
}, [type, filterStatus, filterEnv, searchQuery]);
const fetchInstances = useCallback(async () => {
setLoading(true);
try {
const res = await instanceApi.list({ db_type: type });
const data = Array.isArray(res) ? res : [];
setInstances(data);
} catch {
// keep existing
} finally {
setLoading(false);
}
}, [type]);
const statusCounts = useMemo(() => ({
online: instances.filter(i => i.status === 'online').length,
warning: instances.filter(i => i.status === 'warning').length,
offline: instances.filter(i => i.status === 'offline').length,
}), [instances]);
useEffect(() => { fetchInstances(); }, [fetchInstances]);
const filtered = instances.filter(i => {
if (filterStatus && i.status !== filterStatus) return false;
if (filterEnv && i.env !== filterEnv) return false;
if (searchQuery && !i.name.includes(searchQuery) && !i.host.includes(searchQuery)) return false;
return true;
});
const statusCounts = {
online: filtered.filter(i => i.status === 'online').length,
offline: filtered.filter(i => i.status === 'offline').length,
total: filtered.length,
};
const handleDelete = async (id: number) => {
if (!confirm('确定删除该实例?')) return;
try {
await instanceApi.delete(id);
fetchInstances();
} catch {}
};
if (showCreate) {
return <CreateInstanceForm defaultType={type} onCancel={() => setShowCreate(false)} onCreated={() => { setShowCreate(false); fetchInstances(); }} />;
}
if (view === 'detail' && selectedInstance) {
return <InstanceDetail instance={selectedInstance} onBack={() => setView('grid')} config={config} canEdit={canEdit} canDelete={canDelete} />;
return <InstanceDetail instance={selectedInstance} onBack={() => setView('grid')} config={config} canEdit={canEdit} canDelete={canDelete} onDelete={() => { handleDelete(selectedInstance.id); setView('grid'); }} />;
}
return (
<div className="space-y-5">
<PageHeader
title={`${config.label} 实例管理`}
description={`管理 ${config.label} 数据库实例,监控连接状态和性能指标`}
secondaryAction={{ label: '刷新', icon: RefreshCw, onClick: () => {} }}
action={canAdd ? { label: '添加实例', icon: Plus, onClick: () => {}, permission: 'instance.add' } : undefined}
description={`管理 ${config.label} 数据库实例,监控连接状态`}
secondaryAction={{ label: '刷新', icon: RefreshCw, onClick: fetchInstances }}
action={canAdd ? { label: '添加实例', icon: Plus, onClick: () => setShowCreate(true), permission: 'instance.add' } : undefined}
/>
{/* 状态概览 */}
<div className="grid grid-cols-4 gap-4">
<div className="grid grid-cols-3 gap-4">
<Card className="dg-card">
<CardContent className="p-4 flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-green-500/10 flex items-center justify-center"><Activity className="w-5 h-5 text-green-400" /></div>
<div><div className="text-2xl font-bold">{statusCounts.online}</div><div className="text-xs text-[var(--dg-text-muted)]">在线实例</div></div>
</CardContent>
</Card>
<Card className="dg-card">
<CardContent className="p-4 flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-yellow-500/10 flex items-center justify-center"><HardDrive className="w-5 h-5 text-yellow-400" /></div>
<div><div className="text-2xl font-bold">{statusCounts.warning}</div><div className="text-xs text-[var(--dg-text-muted)]">告警实例</div></div>
</CardContent>
</Card>
<Card className="dg-card">
<CardContent className="p-4 flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-red-500/10 flex items-center justify-center"><HardDrive className="w-5 h-5 text-red-400" /></div>
@@ -85,7 +125,7 @@ export default function InstancePage() {
<Card className="dg-card">
<CardContent className="p-4 flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center"><Server className="w-5 h-5 text-blue-400" /></div>
<div><div className="text-2xl font-bold">{instances.length}</div><div className="text-xs text-[var(--dg-text-muted)]">总实例数</div></div>
<div><div className="text-2xl font-bold">{statusCounts.total}</div><div className="text-xs text-[var(--dg-text-muted)]">总实例数</div></div>
</CardContent>
</Card>
</div>
@@ -94,95 +134,73 @@ export default function InstancePage() {
<div className="flex items-center gap-3 p-4 dg-card flex-wrap">
<div className="relative flex-1 max-w-xs">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--dg-text-muted)]" />
<input
className="w-full pl-10 pr-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)] focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400/20 transition-all"
placeholder="搜索实例名称/地址"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
/>
<input className="w-full pl-10 pr-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm placeholder:text-[var(--dg-text-muted)] focus:border-blue-400 focus:outline-none" placeholder="搜索实例名称/地址" value={searchQuery} onChange={e => setSearchQuery(e.target.value)} />
</div>
<select className="px-3 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-primary)] focus:border-blue-400 focus:outline-none" value={filterStatus} onChange={e => setFilterStatus(e.target.value)}>
<select className="px-3 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" value={filterStatus} onChange={e => setFilterStatus(e.target.value)}>
<option value="">全部状态</option>
<option value="online">在线</option>
<option value="offline">离线</option>
<option value="warning">告警</option>
</select>
<select className="px-3 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-primary)] focus:border-blue-400 focus:outline-none" value={filterEnv} onChange={e => setFilterEnv(e.target.value)}>
<select className="px-3 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm focus:border-blue-400 focus:outline-none" value={filterEnv} onChange={e => setFilterEnv(e.target.value)}>
<option value="">全部环境</option>
<option value="prod">生产</option>
<option value="test">测试</option>
<option value="dev">开发</option>
<option value="production">生产</option>
<option value="staging">预发</option>
<option value="development">开发</option>
</select>
</div>
{/* 实例卡片网格 */}
{instances.length === 0 ? (
{/* 实例卡片 */}
{loading && instances.length === 0 ? (
<div className="p-10 text-center text-[var(--dg-text-muted)]">加载中...</div>
) : filtered.length === 0 ? (
<EmptyState type="search" />
) : (
<div className="grid grid-cols-3 gap-4">
{instances.map(instance => (
<div
key={instance.id}
className="dg-card p-5 cursor-pointer dg-card-hover group"
onClick={() => { setSelectedInstance(instance); setView('detail'); }}
>
<div className="flex items-start justify-between mb-4">
<div className={`w-11 h-11 rounded-xl ${config.bg} flex items-center justify-center`}>
<config.icon className={`w-6 h-6 ${config.color}`} />
</div>
<div className="flex items-center gap-2">
{instance.tags.map(tag => (
<span key={tag} className="px-2 py-0.5 bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)] text-[11px] rounded-md">{tag}</span>
))}
<div className={`w-2.5 h-2.5 rounded-full ${instance.status === 'online' ? 'bg-green-400 shadow-[0_0_0_3px_rgba(63,185,80,0.2)]' : instance.status === 'warning' ? 'bg-yellow-400' : 'bg-red-400'}`} />
</div>
</div>
<h3 className="text-base font-semibold mb-1">{instance.name}</h3>
<p className="text-xs text-[var(--dg-text-muted)] mb-1">{instance.host}:{instance.port}</p>
<p className="text-xs text-[var(--dg-text-muted)] mb-4">{config.label} {instance.version} · {instance.env === 'prod' ? '生产' : instance.env === 'test' ? '测试' : '开发'}</p>
<div className="grid grid-cols-3 gap-3 pt-4 border-t border-[var(--dg-border)]">
<div className="text-center">
<div className="text-lg font-semibold">{instance.connections}</div>
<div className="text-[11px] text-[var(--dg-text-muted)]">连接数</div>
</div>
<div className="text-center">
<div className="text-lg font-semibold">{instance.qps.toLocaleString()}</div>
<div className="text-[11px] text-[var(--dg-text-muted)]">QPS</div>
</div>
<div className="text-center">
<div className="text-lg font-semibold">{instance.latency}<span className="text-xs font-normal text-[var(--dg-text-muted)]">ms</span></div>
<div className="text-[11px] text-[var(--dg-text-muted)]">延迟</div>
</div>
</div>
<div className="mt-4 space-y-2">
<div className="flex items-center gap-2 text-[11px]">
<Cpu className="w-3 h-3 text-[var(--dg-text-muted)]" />
<div className="flex-1 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
<div className="h-full bg-blue-400 rounded-full transition-all" style={{ width: `${instance.cpu}%` }} />
<div className="grid grid-cols-3 gap-4">
{filtered.map(instance => (
<div key={instance.id} className="dg-card p-5 cursor-pointer dg-card-hover group" onClick={() => { setSelectedInstance(instance); setView('detail'); }}>
<div className="flex items-start justify-between mb-4">
<div className={`w-11 h-11 rounded-xl ${config.bg} flex items-center justify-center`}>
<config.icon className={`w-6 h-6 ${config.color}`} />
</div>
<span className="text-[var(--dg-text-muted)] w-8 text-right">{instance.cpu}%</span>
</div>
<div className="flex items-center gap-2 text-[11px]">
<MemoryStick className="w-3 h-3 text-[var(--dg-text-muted)]" />
<div className="flex-1 h-1.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
<div className="h-full bg-purple-400 rounded-full transition-all" style={{ width: `${instance.memory}%` }} />
<div className="flex items-center gap-2">
<span className="px-2 py-0.5 bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)] text-[11px] rounded-md">{envLabel[instance.env] || instance.env}</span>
<div className={`w-2.5 h-2.5 rounded-full ${instance.status === 'online' ? 'bg-green-400 shadow-[0_0_0_3px_rgba(63,185,80,0.2)]' : 'bg-red-400'}`} />
</div>
<span className="text-[var(--dg-text-muted)] w-8 text-right">{instance.memory}%</span>
</div>
<h3 className="text-base font-semibold mb-1">{instance.name}</h3>
<p className="text-xs text-[var(--dg-text-muted)] mb-1">{instance.host}:{instance.port}</p>
<p className="text-xs text-[var(--dg-text-muted)] mb-3">{config.label} {instance.version || '-'} · {envLabel[instance.env] || instance.env}</p>
{instance.description && <p className="text-xs text-[var(--dg-text-secondary)] line-clamp-2">{instance.description}</p>}
</div>
</div>
))}
</div>
))}
</div>
)}
</div>
);
}
function InstanceDetail({ instance, onBack, config, canEdit, canDelete }: {
instance: DBInstance; onBack: () => void; config: { icon: React.ElementType; color: string; bg: string };
canEdit: boolean; canDelete: boolean;
/* ---------- InstanceDetail ---------- */
function InstanceDetail({ instance, onBack, config, canEdit, canDelete, onDelete }: {
instance: InstanceItem; onBack: () => void; config: { icon: React.ElementType; color: string; bg: string };
canEdit: boolean; canDelete: boolean; onDelete: () => void;
}) {
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState<{ success: boolean; message: string; version?: string } | null>(null);
const DetailIcon = config.icon;
const handleTest = async () => {
setTesting(true);
try {
const res = await instanceApi.testConnection(instance.id);
setTestResult(res as any);
} catch (e: any) {
setTestResult({ success: false, message: e?.response?.data?.detail || '连接失败' });
} finally {
setTesting(false);
}
};
return (
<div className="space-y-5">
<div className="flex items-center justify-between">
@@ -196,94 +214,171 @@ function InstanceDetail({ instance, onBack, config, canEdit, canDelete }: {
</div>
<h1 className="text-2xl font-semibold flex items-center gap-3">
{instance.name}
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${instance.status === 'online' ? 'bg-green-500/15 text-green-400' : instance.status === 'warning' ? 'bg-yellow-500/15 text-yellow-400' : 'bg-red-500/15 text-red-400'}`}>
{instance.status === 'online' ? '在线' : instance.status === 'warning' ? '告警' : '离线'}
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${instance.status === 'online' ? 'bg-green-500/15 text-green-400' : 'bg-red-500/15 text-red-400'}`}>
{instance.status === 'online' ? '在线' : '离线'}
</span>
</h1>
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">{instance.host}:{instance.port} · 版本 {instance.version}</p>
<p className="text-sm text-[var(--dg-text-secondary)] mt-1">{instance.host}:{instance.port} · {instance.version || '版本未知'}</p>
</div>
</div>
<div className="flex gap-2">
<button onClick={handleTest} disabled={testing} className="flex items-center gap-2 px-4 py-2.5 bg-blue-500/15 border border-blue-500/30 rounded-lg text-sm font-medium text-blue-400 hover:bg-blue-500/25 transition-all disabled:opacity-50">
<Zap className="w-4 h-4" /> {testing ? '测试中...' : '测试连接'}
</button>
{canEdit && (
<button className="flex items-center gap-2 px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)] transition-all">
<Edit2 className="w-4 h-4" /> 编辑
</button>
)}
{canDelete && (
<button className="flex items-center gap-2 px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm text-red-400 hover:bg-red-500/10 hover:border-red-500/30 transition-all">
<button onClick={onDelete} className="flex items-center gap-2 px-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm text-red-400 hover:bg-red-500/10 hover:border-red-500/30 transition-all">
<Trash2 className="w-4 h-4" /> 删除
</button>
)}
</div>
</div>
{/* 性能指标 */}
<div className="grid grid-cols-6 gap-4">
{[
{ label: '连接数', value: instance.connections, icon: Activity },
{ label: 'QPS', value: instance.qps.toLocaleString(), icon: Zap },
{ label: '延迟', value: `${instance.latency}ms`, icon: Clock },
{ label: 'CPU', value: `${instance.cpu}%`, icon: Cpu },
{ label: '内存', value: `${instance.memory}%`, icon: MemoryStick },
{ label: '磁盘', value: `${instance.disk}%`, icon: HardDrive },
].map((m, i) => (
<Card key={i} className="dg-card">
<CardContent className="p-4 text-center">
<m.icon className="w-5 h-5 text-[var(--dg-text-muted)] mx-auto mb-2" />
<div className="text-xl font-bold">{m.value}</div>
<div className="text-xs text-[var(--dg-text-muted)] mt-1">{m.label}</div>
</CardContent>
</Card>
))}
{/* 连接测试结果 */}
{testResult && (
<Card className={`dg-card ${testResult.success ? 'border-green-500/30' : 'border-red-500/30'}`}>
<CardContent className="p-4 flex items-center gap-3">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${testResult.success ? 'bg-green-500/15' : 'bg-red-500/15'}`}>
{testResult.success ? <Activity className="w-4 h-4 text-green-400" /> : <XCircle className="w-4 h-4 text-red-400" />}
</div>
<div>
<div className={`text-sm font-medium ${testResult.success ? 'text-green-400' : 'text-red-400'}`}>{testResult.message}</div>
{testResult.version && <div className="text-xs text-[var(--dg-text-muted)]">版本: {testResult.version}</div>}
</div>
</CardContent>
</Card>
)}
{/* 连接信息 */}
<Card className="dg-card">
<CardHeader className="border-b border-[var(--dg-border)]"><CardTitle className="text-[15px] font-semibold">连接信息</CardTitle></CardHeader>
<CardContent className="p-5 space-y-3">
{[
{ label: '实例名称', value: instance.name },
{ label: '主机地址', value: instance.host },
{ label: '端口', value: instance.port.toString() },
{ label: '用户名', value: instance.username },
{ label: '默认数据库', value: instance.database || '-' },
{ label: '运行环境', value: envLabel[instance.env] || instance.env },
{ label: '状态', value: instance.status === 'online' ? '在线' : '离线' },
{ label: '版本', value: instance.version || '-' },
{ label: '描述', value: instance.description || '-' },
{ label: '创建时间', value: new Date(instance.created_at).toLocaleString() },
].map(item => (
<div key={item.label} className="flex justify-between py-2 border-b border-[var(--dg-border-light)] last:border-0">
<span className="text-sm text-[var(--dg-text-secondary)]">{item.label}</span>
<span className="text-sm font-medium">{item.value}</span>
</div>
))}
</CardContent>
</Card>
</div>
);
}
/* ---------- CreateInstanceForm ---------- */
function CreateInstanceForm({ defaultType, onCancel, onCreated }: { defaultType: string; onCancel: () => void; onCreated: () => void }) {
const [name, setName] = useState('');
const [dbType, setDbType] = useState(defaultType);
const [host, setHost] = useState('');
const [port, setPort] = useState('3306');
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [database, setDatabase] = useState('');
const [env, setEnv] = useState('development');
const [description, setDescription] = useState('');
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async () => {
if (!name || !host || !port || !username || !password) { setError('请填写所有必填项'); return; }
setSubmitting(true);
setError('');
try {
await instanceApi.create({
name, db_type: dbType, host, port: Number(port),
username, password, database: database || undefined,
env, description: description || undefined,
});
onCreated();
} catch (e: any) {
setError(e?.response?.data?.detail || '创建失败');
} finally {
setSubmitting(false);
}
};
return (
<div className="space-y-5 max-w-3xl">
<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>
</div>
<div className="grid grid-cols-2 gap-5">
{/* 连接信息 */}
<Card className="dg-card">
<CardHeader className="border-b border-[var(--dg-border)]"><CardTitle className="text-[15px] font-semibold">连接信息</CardTitle></CardHeader>
<CardContent className="p-5 space-y-3">
{[
{ label: '主机地址', value: instance.host },
{ label: '端口', value: instance.port.toString() },
{ label: '数据库版本', value: instance.version },
{ label: '运行环境', value: instance.env === 'prod' ? '生产环境' : instance.env === 'test' ? '测试环境' : '开发环境' },
{ label: '运行时间', value: instance.uptime },
{ label: '标签', value: instance.tags.join(', ') },
...(instance.dbType === 'mysql' ? [
{ label: 'Binlog 格式', value: <span className="px-2 py-0.5 bg-green-500/15 text-green-400 text-xs rounded">ROW</span> },
{ label: 'Binlog 状态', value: <span className="px-2 py-0.5 bg-green-500/15 text-green-400 text-xs rounded">ON</span> },
{ label: '回滚支持', value: <span className="flex items-center gap-1"><Undo2 className="w-3 h-3 text-green-400" /> <span className="text-green-400 text-xs">支持</span></span> },
] : []),
].map(item => (
<div key={item.label} className="flex justify-between py-2 border-b border-[var(--dg-border-light)] last:border-0">
<span className="text-sm text-[var(--dg-text-secondary)]">{item.label}</span>
<span className="text-sm font-medium">{item.value}</span>
</div>
))}
</CardContent>
</Card>
<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">
<CardHeader className="border-b border-[var(--dg-border)]"><CardTitle className="text-[15px] font-semibold">资源使用</CardTitle></CardHeader>
<CardContent className="p-5 space-y-5">
{[
{ label: 'CPU 使用率', value: instance.cpu, color: 'bg-blue-400' },
{ label: '内存使用率', value: instance.memory, color: 'bg-purple-400' },
{ label: '磁盘使用率', value: instance.disk, color: 'bg-orange-400' },
].map(item => (
<div key={item.label}>
<div className="flex justify-between text-sm mb-2">
<span className="text-[var(--dg-text-secondary)]">{item.label}</span>
<span className="font-medium">{item.value}%</span>
</div>
<div className="h-2.5 bg-[var(--dg-bg-tertiary)] rounded-full overflow-hidden">
<div className={`h-full ${item.color} rounded-full transition-all`} style={{ width: `${item.value}%` }} />
</div>
</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>
</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>
<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" />
</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>
<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>
<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>
<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" />
</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="数据库密码" />
</div>
</div>
<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>}
<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>
</div>
);