CodeReview/frontend/src/pages/Dashboard.tsx

629 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Dashboard Page
* Cyberpunk Terminal Aesthetic
*/
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
LineChart, Line, PieChart, Pie, Cell,
XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer
} from "recharts";
import {
Activity, AlertTriangle, Clock, Code,
FileText, GitBranch, Shield, TrendingUp, Zap,
BarChart3, Target, ArrowUpRight, Calendar,
MessageSquare, Bot, Cpu, Terminal
} from "lucide-react";
import { api, dbMode, isDemoMode } from "@/shared/config/database";
import type { Project, AuditTask, ProjectStats } from "@/shared/types";
import { Link } from "react-router-dom";
import { toast } from "sonner";
import { getRuleSets } from "@/shared/api/rules";
import { getPromptTemplates } from "@/shared/api/prompts";
export default function Dashboard() {
const [stats, setStats] = useState<ProjectStats | null>(null);
const [recentProjects, setRecentProjects] = useState<Project[]>([]);
const [recentTasks, setRecentTasks] = useState<AuditTask[]>([]);
const [loading, setLoading] = useState(true);
const [issueTypeData, setIssueTypeData] = useState<Array<{ name: string; value: number; color: string }>>([]);
const [qualityTrendData, setQualityTrendData] = useState<Array<{ date: string; score: number }>>([]);
const [ruleStats, setRuleStats] = useState({ total: 0, enabled: 0 });
const [templateStats, setTemplateStats] = useState({ total: 0, active: 0 });
useEffect(() => {
loadDashboardData();
}, []);
const loadDashboardData = async () => {
try {
setLoading(true);
const results = await Promise.allSettled([
api.getProjectStats(),
api.getProjects(),
api.getAuditTasks()
]);
if (results[0].status === 'fulfilled') {
setStats(results[0].value);
} else {
setStats({
total_projects: 0,
active_projects: 0,
total_tasks: 0,
completed_tasks: 0,
total_issues: 0,
resolved_issues: 0,
avg_quality_score: 0
});
}
if (results[1].status === 'fulfilled') {
setRecentProjects(Array.isArray(results[1].value) ? results[1].value.slice(0, 6) : []);
} else {
setRecentProjects([]);
}
let tasks: AuditTask[] = [];
if (results[2].status === 'fulfilled') {
tasks = Array.isArray(results[2].value) ? results[2].value : [];
setRecentTasks(tasks.slice(0, 10));
} else {
setRecentTasks([]);
}
if (tasks.length > 0) {
const tasksByDate = tasks
.filter(t => t.completed_at && t.quality_score > 0)
.sort((a, b) => new Date(a.completed_at!).getTime() - new Date(b.completed_at!).getTime())
.slice(-6);
const trendData = tasksByDate.map((task) => ({
date: new Date(task.completed_at!).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' }),
score: task.quality_score
}));
setQualityTrendData(trendData.length > 0 ? trendData : []);
} else {
setQualityTrendData([]);
}
try {
const allIssues = await Promise.all(
tasks.map(task => api.getAuditIssues(task.id).catch(() => []))
);
const flatIssues = allIssues.flat();
if (flatIssues.length > 0) {
const typeCount: Record<string, number> = {};
flatIssues.forEach(issue => {
typeCount[issue.issue_type] = (typeCount[issue.issue_type] || 0) + 1;
});
const typeMap: Record<string, { name: string; color: string }> = {
security: { name: '安全问题', color: '#f43f5e' },
bug: { name: '潜在Bug', color: '#f97316' },
performance: { name: '性能问题', color: '#eab308' },
style: { name: '代码风格', color: '#3b82f6' },
maintainability: { name: '可维护性', color: '#8b5cf6' }
};
const issueData = Object.entries(typeCount).map(([type, count]) => ({
name: typeMap[type]?.name || type,
value: count,
color: typeMap[type]?.color || '#6b7280'
}));
setIssueTypeData(issueData);
} else {
setIssueTypeData([]);
}
} catch (error) {
setIssueTypeData([]);
}
try {
const [rulesRes, promptsRes] = await Promise.all([
getRuleSets(),
getPromptTemplates(),
]);
const totalRules = rulesRes.items.reduce((acc, rs) => acc + rs.rules_count, 0);
const enabledRules = rulesRes.items.reduce((acc, rs) => acc + rs.enabled_rules_count, 0);
setRuleStats({ total: totalRules, enabled: enabledRules });
setTemplateStats({
total: promptsRes.items.length,
active: promptsRes.items.filter(t => t.is_active).length
});
} catch (error) {
console.error('获取规则和模板统计失败:', error);
}
} catch (error) {
console.error('仪表盘数据加载失败:', error);
toast.error("数据加载失败");
} finally {
setLoading(false);
}
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'completed':
return <Badge className="cyber-badge-success"></Badge>;
case 'running':
return <Badge className="cyber-badge-info"></Badge>;
case 'failed':
return <Badge className="cyber-badge-danger"></Badge>;
default:
return <Badge className="cyber-badge-muted"></Badge>;
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center space-y-4">
<div className="loading-spinner mx-auto" />
<p className="text-muted-foreground font-mono text-base uppercase tracking-wider">...</p>
</div>
</div>
);
}
return (
<div className="space-y-6 p-6 bg-background min-h-screen font-mono relative">
{/* Grid background */}
<div className="absolute inset-0 cyber-grid-subtle pointer-events-none" />
{/* Demo Mode Warning */}
{isDemoMode && (
<div className="relative z-10 cyber-card p-4 border-amber-500/30 bg-amber-500/5">
<div className="flex items-start gap-3">
<AlertTriangle className="w-5 h-5 text-amber-400 mt-0.5" />
<div className="text-sm text-foreground/80">
使<span className="text-amber-400 font-bold"></span>
<Link to="/admin" className="ml-2 text-primary font-bold hover:underline">
</Link>
</div>
</div>
</div>
)}
{/* Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative z-10">
{/* Total Projects */}
<div className="cyber-card p-4">
<div className="flex items-center justify-between">
<div>
<p className="stat-label"></p>
<p className="stat-value">{stats?.total_projects || 0}</p>
<p className="text-sm text-emerald-400 mt-1 flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-emerald-400" />
: {stats?.active_projects || 0}
</p>
</div>
<div className="stat-icon text-primary">
<Code className="w-6 h-6" />
</div>
</div>
</div>
{/* Audit Tasks */}
<div className="cyber-card p-4">
<div className="flex items-center justify-between">
<div>
<p className="stat-label"></p>
<p className="stat-value">{stats?.total_tasks || 0}</p>
<p className="text-sm text-emerald-400 mt-1 flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-emerald-400" />
: {stats?.completed_tasks || 0}
</p>
</div>
<div className="stat-icon text-emerald-400">
<Activity className="w-6 h-6" />
</div>
</div>
</div>
{/* Issues Found */}
<div className="cyber-card p-4">
<div className="flex items-center justify-between">
<div>
<p className="stat-label"></p>
<p className="stat-value">{stats?.total_issues || 0}</p>
<p className="text-sm text-amber-400 mt-1 flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-amber-400" />
: {stats?.resolved_issues || 0}
</p>
</div>
<div className="stat-icon text-amber-400">
<AlertTriangle className="w-6 h-6" />
</div>
</div>
</div>
{/* Quality Score */}
<div className="cyber-card p-4">
<div className="flex items-center justify-between">
<div>
<p className="stat-label"></p>
<p className="stat-value">
{stats?.avg_quality_score ? stats.avg_quality_score.toFixed(1) : '0.0'}
</p>
{stats?.avg_quality_score ? (
<p className="text-sm text-emerald-400 mt-1 flex items-center gap-1">
<TrendingUp className="w-4 h-4" />
</p>
) : (
<p className="text-sm text-muted-foreground mt-1"></p>
)}
</div>
<div className="stat-icon text-violet-400">
<Target className="w-6 h-6" />
</div>
</div>
</div>
</div>
{/* Main Content */}
<div className="grid grid-cols-1 xl:grid-cols-4 gap-4 relative z-10">
{/* Left Content */}
<div className="xl:col-span-3 space-y-4">
{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Quality Trend */}
<div className="cyber-card p-4">
<div className="section-header">
<TrendingUp className="w-5 h-5 text-primary" />
<h3 className="section-title"></h3>
</div>
{qualityTrendData.length > 0 ? (
<ResponsiveContainer width="100%" height={220}>
<LineChart data={qualityTrendData}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--cyber-border)" />
<XAxis dataKey="date" stroke="var(--cyber-text-muted)" fontSize={11} tick={{ fontFamily: 'monospace' }} />
<YAxis stroke="var(--cyber-text-muted)" fontSize={11} domain={[0, 100]} tick={{ fontFamily: 'monospace' }} />
<Tooltip
contentStyle={{
backgroundColor: 'var(--cyber-bg-elevated)',
border: '1px solid var(--cyber-border)',
borderRadius: '4px',
fontFamily: 'monospace',
fontSize: '12px',
color: 'var(--cyber-text)'
}}
/>
<Line
type="monotone"
dataKey="score"
stroke="hsl(var(--primary))"
strokeWidth={2}
dot={{ fill: 'hsl(var(--primary))', stroke: 'var(--cyber-bg)', strokeWidth: 2, r: 4 }}
activeDot={{ r: 6, fill: 'hsl(var(--primary))' }}
/>
</LineChart>
</ResponsiveContainer>
) : (
<div className="empty-state h-[220px]">
<TrendingUp className="empty-state-icon" />
<p className="empty-state-description"></p>
</div>
)}
</div>
{/* Issue Distribution */}
<div className="cyber-card p-4">
<div className="section-header">
<BarChart3 className="w-5 h-5 text-violet-400" />
<h3 className="section-title"></h3>
</div>
{issueTypeData.length > 0 ? (
<ResponsiveContainer width="100%" height={220}>
<PieChart>
<Pie
data={issueTypeData}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
outerRadius={70}
dataKey="value"
stroke="var(--cyber-bg)"
strokeWidth={2}
>
{issueTypeData.map((entry) => (
<Cell key={`cell-${entry.name}`} fill={entry.color} />
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: 'var(--cyber-bg-elevated)',
border: '1px solid var(--cyber-border)',
borderRadius: '4px',
fontFamily: 'monospace',
fontSize: '12px',
color: 'var(--cyber-text)'
}}
/>
</PieChart>
</ResponsiveContainer>
) : (
<div className="empty-state h-[220px]">
<BarChart3 className="empty-state-icon" />
<p className="empty-state-description"></p>
</div>
)}
</div>
</div>
{/* Projects Overview */}
<div className="cyber-card p-4">
<div className="section-header">
<FileText className="w-5 h-5 text-primary" />
<h3 className="section-title"></h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{recentProjects.length > 0 ? (
recentProjects.map((project) => (
<Link
key={project.id}
to={`/projects/${project.id}`}
className="block p-4 rounded-lg transition-all group"
style={{
background: 'var(--cyber-bg-elevated)',
border: '1px solid var(--cyber-border)'
}}
onMouseOver={(e) => {
e.currentTarget.style.background = 'var(--cyber-hover-bg)';
e.currentTarget.style.borderColor = 'var(--cyber-border-accent)';
}}
onMouseOut={(e) => {
e.currentTarget.style.background = 'var(--cyber-bg-elevated)';
e.currentTarget.style.borderColor = 'var(--cyber-border)';
}}
>
<div className="flex items-start justify-between mb-2">
<h4 className="font-semibold text-foreground group-hover:text-primary transition-colors truncate">
{project.name}
</h4>
<Badge className={`ml-2 flex-shrink-0 ${project.is_active ? 'cyber-badge-success' : 'cyber-badge-muted'}`}>
{project.is_active ? '活跃' : '暂停'}
</Badge>
</div>
<p className="text-sm text-muted-foreground line-clamp-2 mb-3">
{project.description || '暂无描述'}
</p>
<div className="flex items-center text-sm text-muted-foreground">
<Calendar className="w-4 h-4 mr-1" />
{new Date(project.created_at).toLocaleDateString('zh-CN')}
</div>
</Link>
))
) : (
<div className="col-span-full empty-state">
<Code className="empty-state-icon" />
<p className="empty-state-title"></p>
<p className="empty-state-description"></p>
</div>
)}
</div>
</div>
{/* Recent Tasks */}
<div className="cyber-card p-4">
<div className="section-header">
<Clock className="w-5 h-5 text-emerald-400" />
<h3 className="section-title"></h3>
<Link to="/audit-tasks" className="ml-auto">
<Button variant="ghost" size="sm" className="text-muted-foreground hover:text-foreground">
<ArrowUpRight className="w-3 h-3 ml-1" />
</Button>
</Link>
</div>
<div className="space-y-2">
{recentTasks.length > 0 ? (
recentTasks.slice(0, 6).map((task) => (
<Link
key={task.id}
to={`/tasks/${task.id}`}
className="flex items-center justify-between p-3 rounded-lg transition-all group"
style={{
background: 'var(--cyber-bg-elevated)',
}}
onMouseOver={(e) => {
e.currentTarget.style.background = 'var(--cyber-hover-bg)';
}}
onMouseOut={(e) => {
e.currentTarget.style.background = 'var(--cyber-bg-elevated)';
}}
>
<div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
task.status === 'completed' ? 'bg-emerald-500/20 text-emerald-400' :
task.status === 'running' ? 'bg-sky-500/20 text-sky-400' :
'bg-rose-500/20 text-rose-400'
}`}>
{task.status === 'completed' ? <Activity className="w-4 h-4" /> :
task.status === 'running' ? <Clock className="w-4 h-4" /> :
<AlertTriangle className="w-4 h-4" />}
</div>
<div>
<p className="text-base font-medium text-foreground group-hover:text-primary transition-colors">
{task.project?.name || '未知项目'}
</p>
<p className="text-sm text-muted-foreground">
: <span className="text-foreground">{task.quality_score?.toFixed(1) || '0.0'}</span>
</p>
</div>
</div>
{getStatusBadge(task.status)}
</Link>
))
) : (
<div className="empty-state">
<Activity className="empty-state-icon" />
<p className="empty-state-title"></p>
</div>
)}
</div>
</div>
</div>
{/* Right Sidebar */}
<div className="xl:col-span-1 space-y-4">
{/* Quick Actions */}
<div className="cyber-card p-4">
<div className="section-header">
<Zap className="w-5 h-5 text-primary" />
<h3 className="section-title"></h3>
</div>
<div className="space-y-2">
<Link to="/agent-audit" className="block">
<Button className="w-full justify-start cyber-btn-primary h-10">
<Bot className="w-4 h-4 mr-2" />
Agent
</Button>
</Link>
<Link to="/instant-analysis" className="block">
<Button variant="outline" className="w-full justify-start cyber-btn-outline h-10">
<Zap className="w-4 h-4 mr-2" />
</Button>
</Link>
<Link to="/projects" className="block">
<Button variant="outline" className="w-full justify-start cyber-btn-outline h-10">
<GitBranch className="w-4 h-4 mr-2" />
</Button>
</Link>
<Link to="/audit-tasks" className="block">
<Button variant="outline" className="w-full justify-start cyber-btn-outline h-10">
<Shield className="w-4 h-4 mr-2" />
</Button>
</Link>
</div>
</div>
{/* System Status */}
<div className="cyber-card p-4">
<div className="section-header">
<Cpu className="w-5 h-5 text-emerald-400" />
<h3 className="section-title"></h3>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-base text-muted-foreground"></span>
<Badge className={`
${dbMode === 'api' ? 'cyber-badge-primary' :
dbMode === 'local' ? 'cyber-badge-info' :
dbMode === 'supabase' ? 'cyber-badge-success' :
'cyber-badge-muted'}
`}>
{dbMode === 'api' ? '后端' : dbMode === 'local' ? '本地' : dbMode === 'supabase' ? '云端' : '演示'}
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-base text-muted-foreground"></span>
<span className="text-base font-bold text-foreground">{stats?.active_projects || 0}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-base text-muted-foreground"></span>
<span className="text-base font-bold text-sky-400">
{recentTasks.filter(t => t.status === 'running').length}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-base text-muted-foreground"></span>
<span className="text-base font-bold text-amber-400">
{stats ? stats.total_issues - stats.resolved_issues : 0}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-base text-muted-foreground flex items-center gap-1">
<Shield className="w-4 h-4" />
</span>
<span className="text-base font-bold text-violet-400">
{ruleStats.enabled}/{ruleStats.total}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-base text-muted-foreground flex items-center gap-1">
<MessageSquare className="w-4 h-4" />
</span>
<span className="text-base font-bold text-emerald-400">
{templateStats.active}/{templateStats.total}
</span>
</div>
</div>
</div>
{/* Recent Activity */}
<div className="cyber-card p-4">
<div className="section-header">
<Terminal className="w-5 h-5 text-amber-400" />
<h3 className="section-title"></h3>
</div>
<div className="space-y-2">
{recentTasks.length > 0 ? (
recentTasks.slice(0, 3).map((task) => {
const timeAgo = (() => {
const now = new Date();
const taskDate = new Date(task.created_at);
const diffMs = now.getTime() - taskDate.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 60) return `${diffMins}分钟前`;
if (diffHours < 24) return `${diffHours}小时前`;
return `${diffDays}天前`;
})();
const statusText =
task.status === 'completed' ? '任务完成' :
task.status === 'running' ? '任务运行中' :
task.status === 'failed' ? '任务失败' : '任务待处理';
return (
<Link
key={task.id}
to={`/tasks/${task.id}`}
className={`block p-3 rounded-lg border transition-all ${
task.status === 'completed' ? 'bg-emerald-500/5 border-emerald-500/20 hover:border-emerald-500/40' :
task.status === 'running' ? 'bg-sky-500/5 border-sky-500/20 hover:border-sky-500/40' :
task.status === 'failed' ? 'bg-rose-500/5 border-rose-500/20 hover:border-rose-500/40' :
'bg-muted/30 border-border hover:border-border'
}`}
>
<p className="text-base font-medium text-foreground">{statusText}</p>
<p className="text-sm text-muted-foreground mt-1 line-clamp-1">
"{task.project?.name || '未知项目'}"
{task.status === 'completed' && task.issues_count > 0 &&
` - 发现 ${task.issues_count} 个问题`
}
</p>
<p className="text-sm text-muted-foreground/70 mt-1">{timeAgo}</p>
</Link>
);
})
) : (
<div className="empty-state py-6">
<Clock className="w-10 h-10 text-muted-foreground mb-2" />
<p className="text-base text-muted-foreground"></p>
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}