FastAPI backend with auth, user, instance, ticket, query, dashboard APIs. React + TypeScript frontend with mock data UI (10 pages). SQLite + async SQLAlchemy, JWT auth, role-based access control. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
238 lines
12 KiB
TypeScript
238 lines
12 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import {
|
|
ClipboardList, Clock, CheckCircle, AlertTriangle,
|
|
TrendingUp, TrendingDown, Database
|
|
} from 'lucide-react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { dashboardStats, dbInstances, statusMap, statusColorMap, dbTypeColorMap, tickets } from '@/lib/mock';
|
|
import * as echarts from 'echarts';
|
|
|
|
const stats = [
|
|
{ title: '今日工单', value: dashboardStats.todayTickets, trend: dashboardStats.todayTicketsTrend, icon: ClipboardList, color: 'text-blue-400', bg: 'bg-blue-400/10' },
|
|
{ title: '待审批', value: dashboardStats.pendingApproval, trend: dashboardStats.pendingTrend, icon: Clock, color: 'text-orange-400', bg: 'bg-orange-400/10' },
|
|
{ title: '查询审计', value: dashboardStats.queryAuditCount.toLocaleString(), trend: dashboardStats.queryTrend, icon: CheckCircle, color: 'text-purple-400', bg: 'bg-purple-400/10' },
|
|
{ title: '慢查询', value: dashboardStats.slowQueries, trend: dashboardStats.slowQueryTrend, icon: AlertTriangle, color: 'text-red-400', bg: 'bg-red-400/10' },
|
|
];
|
|
|
|
export default function Dashboard() {
|
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!chartRef.current) return;
|
|
const chart = echarts.init(chartRef.current);
|
|
const option: any = {
|
|
backgroundColor: 'transparent',
|
|
tooltip: {
|
|
trigger: 'axis' as const,
|
|
backgroundColor: 'rgba(22, 27, 34, 0.95)',
|
|
borderColor: '#30363d',
|
|
textStyle: { color: '#f0f6fc' },
|
|
},
|
|
legend: {
|
|
data: ['提交工单', '执行成功', '执行失败'],
|
|
textStyle: { color: '#8b949e' },
|
|
bottom: 0,
|
|
itemWidth: 14,
|
|
itemHeight: 8,
|
|
},
|
|
grid: { left: '2%', right: '3%', bottom: '18%', top: '8%', containLabel: true },
|
|
xAxis: {
|
|
type: 'category' as const,
|
|
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
|
axisLine: { lineStyle: { color: '#30363d' } },
|
|
axisLabel: { color: '#8b949e', fontSize: 11 },
|
|
},
|
|
yAxis: {
|
|
type: 'value' as const,
|
|
axisLine: { show: false },
|
|
splitLine: { lineStyle: { color: '#21262d' } },
|
|
axisLabel: { color: '#8b949e', fontSize: 11 },
|
|
},
|
|
series: [
|
|
{
|
|
name: '提交工单', type: 'line', smooth: true,
|
|
data: [120, 132, 101, 134, 90, 230, 210],
|
|
itemStyle: { color: '#58a6ff' },
|
|
lineStyle: { width: 2 },
|
|
areaStyle: {
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
{ offset: 0, color: 'rgba(88, 166, 255, 0.3)' },
|
|
{ offset: 1, color: 'rgba(88, 166, 255, 0)' },
|
|
]),
|
|
},
|
|
symbol: 'none',
|
|
},
|
|
{
|
|
name: '执行成功', type: 'line', smooth: true,
|
|
data: [110, 125, 95, 128, 85, 215, 200],
|
|
itemStyle: { color: '#3fb950' },
|
|
lineStyle: { width: 2 },
|
|
areaStyle: {
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
{ offset: 0, color: 'rgba(63, 185, 80, 0.3)' },
|
|
{ offset: 1, color: 'rgba(63, 185, 80, 0)' },
|
|
]),
|
|
},
|
|
symbol: 'none',
|
|
},
|
|
{
|
|
name: '执行失败', type: 'line', smooth: true,
|
|
data: [5, 3, 4, 2, 3, 8, 5],
|
|
itemStyle: { color: '#f85149' },
|
|
lineStyle: { width: 2 },
|
|
symbol: 'none',
|
|
},
|
|
],
|
|
};
|
|
chart.setOption(option);
|
|
const handleResize = () => chart.resize();
|
|
window.addEventListener('resize', handleResize);
|
|
return () => { window.removeEventListener('resize', handleResize); chart.dispose(); };
|
|
}, []);
|
|
|
|
const onlineInstances = dbInstances.filter(i => i.status === 'online').length;
|
|
const warningInstances = dbInstances.filter(i => i.status === 'warning').length;
|
|
const offlineInstances = dbInstances.filter(i => i.status === 'offline').length;
|
|
const pendingTickets = tickets.filter(t => t.status === 'pending' || t.status === 'reviewing').slice(0, 4);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* 统计卡片 */}
|
|
<div className="grid grid-cols-4 gap-5">
|
|
{stats.map((s, i) => (
|
|
<Card key={i} className="dg-card dg-card-hover">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<span className="text-sm text-[var(--dg-text-secondary)] font-medium">{s.title}</span>
|
|
<div className={`w-10 h-10 rounded-xl ${s.bg} flex items-center justify-center`}>
|
|
<s.icon className={`w-5 h-5 ${s.color}`} />
|
|
</div>
|
|
</div>
|
|
<div className="text-3xl font-bold text-[var(--dg-text-primary)] mb-2">{s.value}</div>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
<span className={`flex items-center gap-1 font-medium ${s.trend >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
|
{s.trend >= 0 ? <TrendingUp className="w-3.5 h-3.5" /> : <TrendingDown className="w-3.5 h-3.5" />}
|
|
{Math.abs(s.trend)}%
|
|
</span>
|
|
<span className="text-[var(--dg-text-muted)]">较昨日</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{/* 图表 + 实例状态 */}
|
|
<div className="grid grid-cols-3 gap-5">
|
|
{/* 工单趋势图 */}
|
|
<Card className="dg-card col-span-2">
|
|
<CardHeader className="pb-2 border-b border-[var(--dg-border)]">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="text-[15px] font-semibold">工单趋势</CardTitle>
|
|
<div className="flex gap-1.5">
|
|
{['近7天', '近30天', '近90天'].map((p, i) => (
|
|
<button key={p} className={`px-3 py-1.5 text-xs rounded-md transition-colors ${i === 0 ? '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:border-[var(--dg-text-muted)]'}`}>
|
|
{p}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="p-5">
|
|
<div ref={chartRef} style={{ height: 300 }} />
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 实例状态 */}
|
|
<Card className="dg-card">
|
|
<CardHeader className="pb-2 border-b border-[var(--dg-border)]">
|
|
<CardTitle className="text-[15px] font-semibold">实例状态</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
<div className="p-4 border-b border-[var(--dg-border-light)]">
|
|
<div className="grid grid-cols-3 gap-3 text-center">
|
|
<div>
|
|
<div className="text-2xl font-bold text-green-400">{onlineInstances}</div>
|
|
<div className="text-xs text-[var(--dg-text-muted)] mt-1">在线</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-2xl font-bold text-yellow-400">{warningInstances}</div>
|
|
<div className="text-xs text-[var(--dg-text-muted)] mt-1">告警</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-2xl font-bold text-red-400">{offlineInstances}</div>
|
|
<div className="text-xs text-[var(--dg-text-muted)] mt-1">离线</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{dbInstances.slice(0, 4).map(inst => (
|
|
<div key={inst.id} className="flex items-center px-5 py-3.5 border-b border-[var(--dg-border-light)] last:border-0 hover:bg-[var(--dg-bg-hover)] transition-colors cursor-pointer">
|
|
<div className={`w-9 h-9 rounded-lg flex items-center justify-center mr-3.5 ${inst.dbType === 'mysql' ? 'bg-[rgba(0,117,143,0.2)] text-[#26a69a]' : inst.dbType === 'redis' ? 'bg-[rgba(220,50,32,0.2)] text-[#ef5350]' : inst.dbType === 'postgres' ? 'bg-[rgba(51,103,145,0.2)] text-[#42a5f5]' : 'bg-[rgba(71,162,72,0.2)] text-[#66bb6a]'}`}>
|
|
<Database className="w-4.5 h-4.5" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm font-medium truncate">{inst.name}</div>
|
|
<div className="text-xs text-[var(--dg-text-muted)]">{inst.host}:{inst.port}</div>
|
|
</div>
|
|
<span className={`px-2.5 py-1 rounded-full text-[11px] font-medium ${inst.status === 'online' ? 'bg-green-500/15 text-green-400' : inst.status === 'warning' ? 'bg-yellow-500/15 text-yellow-400' : 'bg-red-500/15 text-red-400'}`}>
|
|
{inst.status === 'online' ? '在线' : inst.status === 'warning' ? '告警' : '离线'}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* 待处理工单 */}
|
|
<Card className="dg-card">
|
|
<CardHeader className="pb-2 border-b border-[var(--dg-border)]">
|
|
<CardTitle className="text-[15px] font-semibold">待处理工单</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]">
|
|
<th className="text-left px-5 py-3 font-semibold text-[var(--dg-text-primary)]">工单号</th>
|
|
<th className="text-left px-5 py-3 font-semibold text-[var(--dg-text-primary)]">标题</th>
|
|
<th className="text-left px-5 py-3 font-semibold text-[var(--dg-text-primary)]">数据库</th>
|
|
<th className="text-left px-5 py-3 font-semibold text-[var(--dg-text-primary)]">申请人</th>
|
|
<th className="text-left px-5 py-3 font-semibold text-[var(--dg-text-primary)]">状态</th>
|
|
<th className="text-left px-5 py-3 font-semibold text-[var(--dg-text-primary)]">创建时间</th>
|
|
<th className="text-left px-5 py-3 font-semibold text-[var(--dg-text-primary)]">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{pendingTickets.map(ticket => (
|
|
<tr key={ticket.id} className="border-b border-[var(--dg-border-light)] hover:bg-[var(--dg-bg-hover)] transition-colors">
|
|
<td className="px-5 py-3.5 font-mono text-xs">{ticket.id}</td>
|
|
<td className="px-5 py-3.5 max-w-xs truncate">{ticket.title}</td>
|
|
<td className="px-5 py-3.5">
|
|
<span className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-medium ${dbTypeColorMap[ticket.dbType] || ''}`}>
|
|
{ticket.dbType.toUpperCase()}
|
|
</span>
|
|
</td>
|
|
<td className="px-5 py-3.5">{ticket.applicant}</td>
|
|
<td className="px-5 py-3.5">
|
|
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${statusColorMap[ticket.status] || ''}`}>
|
|
<span className={`w-1.5 h-1.5 rounded-full ${ticket.status === 'reviewing' ? 'bg-blue-400 animate-pulse' : ticket.status === 'pending' ? 'bg-yellow-400' : ''}`} />
|
|
{statusMap[ticket.status]}
|
|
</span>
|
|
</td>
|
|
<td className="px-5 py-3.5 text-[var(--dg-text-secondary)]">{ticket.createTime}</td>
|
|
<td className="px-5 py-3.5">
|
|
<div className="flex gap-2">
|
|
<button className="text-blue-400 hover:text-blue-300 text-xs font-medium transition-colors">查看</button>
|
|
<button className="text-green-400 hover:text-green-300 text-xs font-medium transition-colors">审批</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|