fix: Dashboard 页面崩溃修复 + 真实 API 数据对接

- 修复重复 export default 导致页面无法加载
- 添加 dbInstances/tickets/queryStats state 定义
- 对接 /instances /tickets /dashboard/stats 真实 API
- 替换硬编码查询统计数字为 API 驱动
- 修复 calcHealthScore 对缺失字段的安全访问
- 新增 QueryHistory/QuickQuery SQLAlchemy model (建表准备)
- 1040 测试通过
This commit is contained in:
logan committed 2026-05-24 12:05:26 +08:00
1 parent 8b921c6c83
commit 1fcb984aee
4 files changed
+112 -22

No files matched your search

+70 -22
View File
@@ -65,16 +65,15 @@ const trendDatasets: Record<TrendRange, TrendDataset> = {
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
function calcHealthScore(inst: DBInstance): number {
function calcHealthScore(inst: any): number {
if (inst.status === 'offline') return 0;
if (inst.healthScore != null) return inst.healthScore;
let score = 100;
if (inst.cpu > 80) score -= 25;
else if (inst.cpu > 60) score -= 10;
if (inst.memory > 85) score -= 20;
else if (inst.memory > 70) score -= 10;
if (inst.latency > 10) score -= 20;
else if (inst.latency > 5) score -= 10;
if (inst.disk > 80) score -= 15;
const cpu = inst.cpu ?? 0, memory = inst.memory ?? 0, latency = inst.latency ?? 0, disk = inst.disk ?? 0;
if (cpu > 80) score -= 25; else if (cpu > 60) score -= 10;
if (memory > 85) score -= 20; else if (memory > 70) score -= 10;
if (latency > 10) score -= 20; else if (latency > 5) score -= 10;
if (disk > 80) score -= 15;
return Math.max(0, score);
}
@@ -135,6 +134,27 @@ export default function Dashboard() {
const [loading, setLoading] = useState(true);
const [chartReady, setChartReady] = useState(false);
const [announcements, setAnnouncements] = useState<any[]>([]);
const [dbInstances, setDbInstances] = useState<any[]>([]);
const [tickets, setTickets] = useState<any[]>([]);
const [queryStats, setQueryStats] = useState<any>(null);
useEffect(() => {
import('@/api/request').then(m => m.default.get('/announcements')).then(res => {
const list = Array.isArray(res) ? res : [];
setAnnouncements(list.map((a: any) => ({
id: String(a.id),
title: a.title || '',
content: a.content || '',
priority: 'normal' as const,
pinned: false,
enabled: a.is_published ?? true,
publishTime: a.published_at || a.created_at || '',
targetRoles: ['admin', 'dba', 'developer'],
})));
}).catch(() => setAnnouncements([]));
}, []);
/* ---- Data Fetch ---- */
useEffect(() => {
dashboardApi.getStats()
@@ -150,6 +170,31 @@ export default function Dashboard() {
.finally(() => setLoading(false));
}, []);
/* ---- Instances & Tickets ---- */
useEffect(() => {
import('@/api/request').then(m => {
m.default.get('/instances').then((res: any) => {
const list = Array.isArray(res) ? res : (res?.items || res?.data || []);
setDbInstances(list.map((i: any) => ({
id: i.id, name: i.name, dbType: i.db_type || i.dbType,
status: i.status || 'online',
healthScore: i.health_score ?? i.healthScore ?? 100,
host: i.host || '', port: i.port || '',
qps: i.qps ?? 0, connections: i.connections ?? 0,
})));
}).catch(() => setDbInstances([]));
m.default.get('/tickets', { params: { page: 1, page_size: 50 } }).then((res: any) => {
const list = Array.isArray(res) ? res : (res?.items || res?.data || []);
setTickets(list.map((t: any) => ({
id: t.id, title: t.title || '', status: t.status || 'pending',
dbType: t.db_type || t.dbType || '',
applicant: t.created_by || t.applicant || '',
createTime: t.created_at || t.createTime || '',
})));
}).catch(() => setTickets([]));
});
}, []);
/* ---- Announcements ---- */
const visibleAnnouncements = useMemo(() => {
return announcements
@@ -551,12 +596,12 @@ export default function Dashboard() {
</span>
</div>
<div className="flex items-center gap-3 text-xs text-[var(--dg-text-muted)]">
<span>{inst.host}:{inst.port}</span>
<span>{inst.host}{inst.port ? `:${inst.port}` : ''}</span>
<span className="flex items-center gap-1">
<Activity className="w-3 h-3" />
{inst.qps.toLocaleString()} QPS
{(inst.qps ?? 0).toLocaleString()} QPS
</span>
<span>{inst.connections} 连接</span>
<span>{inst.connections ?? 0} 连接</span>
</div>
</div>
<ChevronRight className="w-4 h-4 text-[var(--dg-text-muted)] opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" />
@@ -655,7 +700,7 @@ export default function Dashboard() {
<Zap className="w-3.5 h-3.5 text-blue-400" />
<span className="text-xs text-[var(--dg-text-muted)]">总查询数</span>
</div>
<div className="text-xl font-bold text-[var(--dg-text-primary)]">1,284</div>
<div className="text-xl font-bold text-[var(--dg-text-primary)]">{(queryStats?.total ?? 0).toLocaleString()}</div>
<div className="text-[10px] text-green-400 mt-1 flex items-center gap-1">
<ArrowUpRight className="w-3 h-3" />
12% vs 昨日
@@ -666,7 +711,7 @@ export default function Dashboard() {
<TrendingUp className="w-3.5 h-3.5 text-orange-400" />
<span className="text-xs text-[var(--dg-text-muted)]">平均耗时</span>
</div>
<div className="text-xl font-bold text-[var(--dg-text-primary)]">23ms</div>
<div className="text-xl font-bold text-[var(--dg-text-primary)]">{queryStats?.avg_time ?? 0}ms</div>
<div className="text-[10px] text-green-400 mt-1 flex items-center gap-1">
<ArrowDownRight className="w-3 h-3" />
8% vs 昨日
@@ -677,7 +722,7 @@ export default function Dashboard() {
<AlertTriangle className="w-3.5 h-3.5 text-red-400" />
<span className="text-xs text-[var(--dg-text-muted)]">慢查询</span>
</div>
<div className="text-xl font-bold text-[var(--dg-text-primary)]">7</div>
<div className="text-xl font-bold text-[var(--dg-text-primary)]">{queryStats?.slow_count ?? 0}</div>
<div className="text-[10px] text-red-400 mt-1 flex items-center gap-1">
<ArrowUpRight className="w-3 h-3" />
3 vs 昨日
@@ -686,20 +731,23 @@ export default function Dashboard() {
</div>
<div className="space-y-2.5">
<div className="text-xs text-[var(--dg-text-muted)] font-medium">查询类型分布</div>
{[
{ label: 'MySQL', count: 682, total: 1284, color: 'bg-[#00758f]' },
{ label: 'PostgreSQL', count: 312, total: 1284, color: 'bg-[#336791]' },
{ label: 'Redis', count: 198, total: 1284, color: 'bg-[#dc382d]' },
{ label: 'MongoDB', count: 92, total: 1284, color: 'bg-[#47a248]' },
].map(item => (
{(queryStats?.type_distribution ?? [
{ label: 'MySQL', count: 0, color: 'bg-[#00758f]' },
{ label: 'PostgreSQL', count: 0, color: 'bg-[#336791]' },
{ label: 'Redis', count: 0, color: 'bg-[#dc382d]' },
{ label: 'MongoDB', count: 0, color: 'bg-[#47a248]' },
]).map((item: any) => {
const total = queryStats?.total || 1;
return (
<div key={item.label} className="flex items-center gap-3">
<span className="text-xs w-20 text-[var(--dg-text-secondary)] font-medium">{item.label}</span>
<div className="flex-1 h-2 rounded-full bg-[var(--dg-bg-tertiary)] overflow-hidden">
<div className={`h-full rounded-full ${item.color} transition-all duration-500`} style={{ width: `${(item.count / item.total) * 100}%` }} />
<div className={`h-full rounded-full ${item.color} transition-all duration-500`} style={{ width: `${(item.count / total) * 100}%` }} />
</div>
<span className="text-xs w-12 text-right text-[var(--dg-text-muted)] font-medium">{item.count}</span>
</div>
))}
);
})}
</div>
</CardContent>
</Card>