feat(audit): Add CreateTaskDialog component for audit task creation

- Implement new CreateTaskDialog component for creating audit tasks
- Add comprehensive form with project selection, task configuration options
- Include dynamic project loading and search functionality
- Implement task creation logic with validation and error handling
- Add support for custom exclude patterns and scan configuration
- Integrate with existing API for task creation
- Enhance user experience with responsive design and informative UI elements
This commit is contained in:
lintsinghua 2025-10-22 21:33:10 +08:00
parent d6a78817e1
commit c287861f5f
3 changed files with 1006 additions and 372 deletions

View File

@ -0,0 +1,537 @@
import { useState, useEffect } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
GitBranch,
Settings,
FileText,
AlertCircle,
Info,
Zap,
Shield,
Search
} from "lucide-react";
import { api } from "@/shared/config/database";
import type { Project, CreateAuditTaskForm } from "@/shared/types";
import { toast } from "sonner";
interface CreateTaskDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onTaskCreated: () => void;
}
export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated }: CreateTaskDialogProps) {
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(false);
const [creating, setCreating] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [taskForm, setTaskForm] = useState<CreateAuditTaskForm>({
project_id: "",
task_type: "repository",
branch_name: "main",
exclude_patterns: ["node_modules/**", ".git/**", "dist/**", "build/**", "*.log"],
scan_config: {
include_tests: true,
include_docs: false,
max_file_size: 1024, // KB
analysis_depth: "standard"
}
});
const commonExcludePatterns = [
{ label: "node_modules", value: "node_modules/**", description: "Node.js 依赖包" },
{ label: ".git", value: ".git/**", description: "Git 版本控制文件" },
{ label: "dist/build", value: "dist/**", description: "构建输出目录" },
{ label: "logs", value: "*.log", description: "日志文件" },
{ label: "cache", value: ".cache/**", description: "缓存文件" },
{ label: "temp", value: "temp/**", description: "临时文件" },
{ label: "vendor", value: "vendor/**", description: "第三方库" },
{ label: "coverage", value: "coverage/**", description: "测试覆盖率报告" }
];
useEffect(() => {
if (open) {
loadProjects();
}
}, [open]);
const loadProjects = async () => {
try {
setLoading(true);
const data = await api.getProjects();
setProjects(data.filter(p => p.is_active));
} catch (error) {
console.error('Failed to load projects:', error);
toast.error("加载项目失败");
} finally {
setLoading(false);
}
};
const handleCreateTask = async () => {
if (!taskForm.project_id) {
toast.error("请选择项目");
return;
}
if (taskForm.task_type === "repository" && !taskForm.branch_name?.trim()) {
toast.error("请输入分支名称");
return;
}
try {
setCreating(true);
await api.createAuditTask({
...taskForm,
created_by: "system" // 无登录场景下使用系统用户
} as any);
toast.success("审计任务创建成功");
onOpenChange(false);
resetForm();
onTaskCreated();
} catch (error) {
console.error('Failed to create task:', error);
toast.error("创建任务失败");
} finally {
setCreating(false);
}
};
const resetForm = () => {
setTaskForm({
project_id: "",
task_type: "repository",
branch_name: "main",
exclude_patterns: ["node_modules/**", ".git/**", "dist/**", "build/**", "*.log"],
scan_config: {
include_tests: true,
include_docs: false,
max_file_size: 1024,
analysis_depth: "standard"
}
});
setSearchTerm("");
};
const toggleExcludePattern = (pattern: string) => {
const patterns = taskForm.exclude_patterns || [];
if (patterns.includes(pattern)) {
setTaskForm({
...taskForm,
exclude_patterns: patterns.filter(p => p !== pattern)
});
} else {
setTaskForm({
...taskForm,
exclude_patterns: [...patterns, pattern]
});
}
};
const addCustomPattern = (pattern: string) => {
if (pattern.trim() && !taskForm.exclude_patterns.includes(pattern.trim())) {
setTaskForm({
...taskForm,
exclude_patterns: [...taskForm.exclude_patterns, pattern.trim()]
});
}
};
const removeExcludePattern = (pattern: string) => {
setTaskForm({
...taskForm,
exclude_patterns: taskForm.exclude_patterns.filter(p => p !== pattern)
});
};
const selectedProject = projects.find(p => p.id === taskForm.project_id);
const filteredProjects = projects.filter(project =>
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
project.description?.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center space-x-2">
<Shield className="w-5 h-5 text-primary" />
<span></span>
</DialogTitle>
</DialogHeader>
<div className="space-y-6">
{/* 项目选择 */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label className="text-base font-medium"></Label>
<Badge variant="outline" className="text-xs">
{filteredProjects.length}
</Badge>
</div>
{/* 项目搜索 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="搜索项目名称..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
{/* 项目列表 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-60 overflow-y-auto">
{loading ? (
<div className="col-span-2 flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
) : filteredProjects.length > 0 ? (
filteredProjects.map((project) => (
<Card
key={project.id}
className={`cursor-pointer transition-all hover:shadow-md ${
taskForm.project_id === project.id
? 'ring-2 ring-primary bg-primary/5'
: 'hover:bg-gray-50'
}`}
onClick={() => setTaskForm({ ...taskForm, project_id: project.id })}
>
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="font-medium text-sm">{project.name}</h4>
{project.description && (
<p className="text-xs text-gray-500 mt-1 line-clamp-2">
{project.description}
</p>
)}
<div className="flex items-center space-x-4 mt-2 text-xs text-gray-400">
<span>{project.repository_type?.toUpperCase() || 'OTHER'}</span>
<span>{project.default_branch}</span>
</div>
</div>
{taskForm.project_id === project.id && (
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center">
<div className="w-2 h-2 rounded-full bg-white"></div>
</div>
)}
</div>
</CardContent>
</Card>
))
) : (
<div className="col-span-2 text-center py-8 text-gray-500">
<FileText className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">
{searchTerm ? '未找到匹配的项目' : '暂无可用项目'}
</p>
</div>
)}
</div>
</div>
{/* 任务配置 */}
{selectedProject && (
<Tabs defaultValue="basic" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="basic" className="flex items-center space-x-2">
<GitBranch className="w-4 h-4" />
<span></span>
</TabsTrigger>
<TabsTrigger value="exclude" className="flex items-center space-x-2">
<FileText className="w-4 h-4" />
<span></span>
</TabsTrigger>
<TabsTrigger value="advanced" className="flex items-center space-x-2">
<Settings className="w-4 h-4" />
<span></span>
</TabsTrigger>
</TabsList>
<TabsContent value="basic" className="space-y-4 mt-6">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="task_type"></Label>
<Select
value={taskForm.task_type}
onValueChange={(value: any) => setTaskForm({ ...taskForm, task_type: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="repository">
<div className="flex items-center space-x-2">
<GitBranch className="w-4 h-4" />
<span></span>
</div>
</SelectItem>
<SelectItem value="instant">
<div className="flex items-center space-x-2">
<Zap className="w-4 h-4" />
<span></span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{taskForm.task_type === "repository" && (
<div className="space-y-2">
<Label htmlFor="branch_name"></Label>
<Input
id="branch_name"
value={taskForm.branch_name || ""}
onChange={(e) => setTaskForm({ ...taskForm, branch_name: e.target.value })}
placeholder={selectedProject.default_branch || "main"}
/>
</div>
)}
</div>
{/* 项目信息展示 */}
<Card className="bg-blue-50 border-blue-200">
<CardContent className="p-4">
<div className="flex items-start space-x-3">
<Info className="w-5 h-5 text-blue-600 mt-0.5" />
<div className="text-sm">
<p className="font-medium text-blue-900 mb-1">{selectedProject.name}</p>
<div className="text-blue-700 space-y-1">
{selectedProject.description && (
<p>{selectedProject.description}</p>
)}
<p>{selectedProject.default_branch}</p>
{selectedProject.programming_languages && (
<p>{JSON.parse(selectedProject.programming_languages).join(', ')}</p>
)}
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="exclude" className="space-y-4 mt-6">
<div className="space-y-4">
<div>
<Label className="text-base font-medium"></Label>
<p className="text-sm text-gray-500 mt-1">
</p>
</div>
{/* 常用排除模式 */}
<div className="grid grid-cols-2 gap-3">
{commonExcludePatterns.map((pattern) => (
<div key={pattern.value} className="flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50">
<Checkbox
checked={taskForm.exclude_patterns.includes(pattern.value)}
onCheckedChange={() => toggleExcludePattern(pattern.value)}
/>
<div className="flex-1">
<p className="text-sm font-medium">{pattern.label}</p>
<p className="text-xs text-gray-500">{pattern.description}</p>
</div>
</div>
))}
</div>
{/* 自定义排除模式 */}
<div className="space-y-2">
<Label></Label>
<div className="flex space-x-2">
<Input
placeholder="例如: *.tmp, test/**"
onKeyPress={(e) => {
if (e.key === 'Enter') {
addCustomPattern(e.currentTarget.value);
e.currentTarget.value = '';
}
}}
/>
<Button
type="button"
variant="outline"
onClick={(e) => {
const input = e.currentTarget.previousElementSibling as HTMLInputElement;
addCustomPattern(input.value);
input.value = '';
}}
>
</Button>
</div>
</div>
{/* 已选择的排除模式 */}
{taskForm.exclude_patterns.length > 0 && (
<div className="space-y-2">
<Label></Label>
<div className="flex flex-wrap gap-2">
{taskForm.exclude_patterns.map((pattern) => (
<Badge
key={pattern}
variant="secondary"
className="cursor-pointer hover:bg-red-100 hover:text-red-800"
onClick={() => removeExcludePattern(pattern)}
>
{pattern} ×
</Badge>
))}
</div>
</div>
)}
</div>
</TabsContent>
<TabsContent value="advanced" className="space-y-4 mt-6">
<div className="space-y-6">
<div>
<Label className="text-base font-medium"></Label>
<p className="text-sm text-gray-500 mt-1">
</p>
</div>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-4">
<div className="flex items-center space-x-3">
<Checkbox
checked={taskForm.scan_config.include_tests}
onCheckedChange={(checked) =>
setTaskForm({
...taskForm,
scan_config: { ...taskForm.scan_config, include_tests: !!checked }
})
}
/>
<div>
<p className="text-sm font-medium"></p>
<p className="text-xs text-gray-500"> *test*, *spec* </p>
</div>
</div>
<div className="flex items-center space-x-3">
<Checkbox
checked={taskForm.scan_config.include_docs}
onCheckedChange={(checked) =>
setTaskForm({
...taskForm,
scan_config: { ...taskForm.scan_config, include_docs: !!checked }
})
}
/>
<div>
<p className="text-sm font-medium"></p>
<p className="text-xs text-gray-500"> README, docs </p>
</div>
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="max_file_size"> (KB)</Label>
<Input
id="max_file_size"
type="number"
value={taskForm.scan_config.max_file_size}
onChange={(e) =>
setTaskForm({
...taskForm,
scan_config: {
...taskForm.scan_config,
max_file_size: parseInt(e.target.value) || 1024
}
})
}
min="1"
max="10240"
/>
</div>
<div className="space-y-2">
<Label htmlFor="analysis_depth"></Label>
<Select
value={taskForm.scan_config.analysis_depth}
onValueChange={(value: any) =>
setTaskForm({
...taskForm,
scan_config: { ...taskForm.scan_config, analysis_depth: value }
})
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="basic"></SelectItem>
<SelectItem value="standard"></SelectItem>
<SelectItem value="deep"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
{/* 分析深度说明 */}
<Card className="bg-amber-50 border-amber-200">
<CardContent className="p-4">
<div className="flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
<div className="text-sm">
<p className="font-medium text-amber-900 mb-2"></p>
<ul className="text-amber-800 space-y-1 text-xs">
<li> <strong></strong></li>
<li> <strong></strong></li>
<li> <strong></strong></li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
)}
{/* 操作按钮 */}
<div className="flex justify-end space-x-3 pt-4 border-t">
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={creating}>
</Button>
<Button
onClick={handleCreateTask}
disabled={!taskForm.project_id || creating}
className="btn-primary"
>
{creating ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
...
</>
) : (
<>
<Shield className="w-4 h-4 mr-2" />
</>
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -9,20 +9,22 @@ import {
CheckCircle,
Clock,
Search,
Play,
FileText,
Calendar
Calendar,
Plus
} from "lucide-react";
import { api } from "@/shared/config/database";
import type { AuditTask } from "@/shared/types";
import { Link } from "react-router-dom";
import { toast } from "sonner";
import CreateTaskDialog from "@/components/audit/CreateTaskDialog";
export default function AuditTasks() {
const [tasks, setTasks] = useState<AuditTask[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState<string>("all");
const [showCreateDialog, setShowCreateDialog] = useState(false);
useEffect(() => {
loadTasks();
@ -92,8 +94,8 @@ export default function AuditTasks() {
<h1 className="page-title"></h1>
<p className="page-subtitle"></p>
</div>
<Button className="btn-primary">
<Play className="w-4 h-4 mr-2" />
<Button className="btn-primary" onClick={() => setShowCreateDialog(true)}>
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
@ -235,28 +237,43 @@ export default function AuditTasks() {
</Badge>
</div>
<div className="grid grid-cols-3 md:grid-cols-5 gap-6 mb-6">
<div className="text-center">
<p className="text-2xl font-bold text-gray-900">{task.total_files}</p>
<p className="text-xs text-gray-500 mt-1"></p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="text-center p-4 bg-gradient-to-br from-blue-50 to-blue-100/30 rounded-xl border border-blue-200">
<div className="text-2xl font-bold text-blue-600 mb-1">{task.total_files}</div>
<p className="text-xs text-blue-700 font-medium"></p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-gray-900">{task.total_lines}</p>
<p className="text-xs text-gray-500 mt-1"></p>
<div className="text-center p-4 bg-gradient-to-br from-purple-50 to-purple-100/30 rounded-xl border border-purple-200">
<div className="text-2xl font-bold text-purple-600 mb-1">{task.total_lines.toLocaleString()}</div>
<p className="text-xs text-purple-700 font-medium"></p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-orange-600">{task.issues_count}</p>
<p className="text-xs text-gray-500 mt-1"></p>
<div className="text-center p-4 bg-gradient-to-br from-orange-50 to-orange-100/30 rounded-xl border border-orange-200">
<div className="text-2xl font-bold text-orange-600 mb-1">{task.issues_count}</div>
<p className="text-xs text-orange-700 font-medium"></p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-primary">{task.quality_score.toFixed(1)}</p>
<p className="text-xs text-gray-500 mt-1"></p>
<div className="text-center p-4 bg-gradient-to-br from-green-50 to-green-100/30 rounded-xl border border-green-200">
<div className="text-2xl font-bold text-green-600 mb-1">{task.quality_score.toFixed(1)}</div>
<p className="text-xs text-green-700 font-medium"></p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-emerald-600">
{Math.round((task.scanned_files / task.total_files) * 100)}%
</p>
<p className="text-xs text-gray-500 mt-1"></p>
</div>
{/* 扫描进度 */}
<div className="mb-6">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700"></span>
<span className="text-sm text-gray-500">
{task.scanned_files} / {task.total_files}
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-gradient-to-r from-primary to-accent h-2 rounded-full transition-all duration-300"
style={{ width: `${Math.round((task.scanned_files / task.total_files) * 100)}%` }}
></div>
</div>
<div className="text-right mt-1">
<span className="text-xs text-gray-500">
{Math.round((task.scanned_files / task.total_files) * 100)}%
</span>
</div>
</div>
@ -307,14 +324,21 @@ export default function AuditTasks() {
{searchTerm || statusFilter !== "all" ? '尝试调整搜索条件或筛选器' : '创建第一个审计任务开始代码质量分析'}
</p>
{!searchTerm && statusFilter === "all" && (
<Button className="btn-primary">
<Play className="w-4 h-4 mr-2" />
<Button className="btn-primary" onClick={() => setShowCreateDialog(true)}>
<Plus className="w-4 h-4 mr-2" />
</Button>
)}
</CardContent>
</Card>
)}
{/* 新建任务对话框 */}
<CreateTaskDialog
open={showCreateDialog}
onOpenChange={setShowCreateDialog}
onTaskCreated={loadTasks}
/>
</div>
);
}

View File

@ -6,78 +6,35 @@ import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
ArrowLeft,
ArrowLeft,
Activity,
AlertTriangle,
CheckCircle,
Clock,
FileText,
Calendar,
GitBranch,
Shield,
Bug,
TrendingUp,
Download,
Code,
Zap,
Lightbulb,
Info,
Lightbulb
Zap
} from "lucide-react";
import { api } from "@/shared/config/database";
import type { AuditTask, AuditIssue } from "@/shared/types";
import { toast } from "sonner";
export default function TaskDetail() {
const { id } = useParams<{ id: string }>();
const [task, setTask] = useState<AuditTask | null>(null);
const [issues, setIssues] = useState<AuditIssue[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (id) {
loadTaskData();
}
}, [id]);
const loadTaskData = async () => {
if (!id) return;
try {
setLoading(true);
const [taskData, issuesData] = await Promise.all([
api.getAuditTaskById(id),
api.getAuditIssues(id)
]);
setTask(taskData);
setIssues(issuesData);
} catch (error) {
console.error('Failed to load task data:', error);
toast.error("加载任务数据失败");
} finally {
setLoading(false);
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed': return 'bg-green-100 text-green-800';
case 'running': return 'bg-red-50 text-red-800';
case 'failed': return 'bg-red-100 text-red-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'completed': return <CheckCircle className="w-5 h-5" />;
case 'running': return <Activity className="w-5 h-5" />;
case 'failed': return <AlertTriangle className="w-5 h-5" />;
default: return <Clock className="w-5 h-5" />;
}
};
// 问题列表组件
function IssuesList({ issues }: { issues: AuditIssue[] }) {
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'critical': return 'bg-red-100 text-red-800 border-red-200';
case 'high': return 'bg-orange-100 text-orange-800 border-orange-200';
case 'medium': return 'bg-yellow-100 text-yellow-800 border-yellow-200';
case 'low': return 'bg-red-50 text-red-800 border-red-200';
case 'low': return 'bg-blue-100 text-blue-800 border-blue-200';
default: return 'bg-gray-100 text-gray-800 border-gray-200';
}
};
@ -93,6 +50,229 @@ export default function TaskDetail() {
}
};
const criticalIssues = issues.filter(issue => issue.severity === 'critical');
const highIssues = issues.filter(issue => issue.severity === 'high');
const mediumIssues = issues.filter(issue => issue.severity === 'medium');
const lowIssues = issues.filter(issue => issue.severity === 'low');
const renderIssue = (issue: AuditIssue, index: number) => (
<div key={issue.id || index} className="border border-gray-200 rounded-xl p-6 hover:shadow-md transition-all duration-200 bg-white">
<div className="flex items-start justify-between mb-4">
<div className="flex items-start space-x-3">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
issue.severity === 'critical' ? 'bg-red-100 text-red-600' :
issue.severity === 'high' ? 'bg-orange-100 text-orange-600' :
issue.severity === 'medium' ? 'bg-yellow-100 text-yellow-600' :
'bg-blue-100 text-blue-600'
}`}>
{getTypeIcon(issue.issue_type)}
</div>
<div>
<h4 className="font-semibold text-lg text-gray-900 mb-1">{issue.title}</h4>
<p className="text-gray-600 text-sm">{issue.file_path}</p>
{issue.line_number && (
<p className="text-gray-500 text-xs mt-1">
{issue.line_number}
{issue.column_number && `, 第 ${issue.column_number}`}
</p>
)}
</div>
</div>
<Badge className={`${getSeverityColor(issue.severity)} px-3 py-1`}>
{issue.severity === 'critical' ? '严重' :
issue.severity === 'high' ? '高' :
issue.severity === 'medium' ? '中等' : '低'}
</Badge>
</div>
{issue.description && (
<p className="text-gray-700 mb-4 leading-relaxed">
{issue.description}
</p>
)}
{issue.code_snippet && (
<div className="bg-gray-900 rounded-lg p-4 mb-4">
<div className="flex items-center justify-between mb-2">
<span className="text-gray-300 text-sm font-medium"></span>
{issue.line_number && (
<span className="text-gray-400 text-xs"> {issue.line_number} </span>
)}
</div>
<pre className="text-sm text-gray-100 overflow-x-auto">
<code>{issue.code_snippet}</code>
</pre>
</div>
)}
<div className="grid md:grid-cols-2 gap-4">
{issue.suggestion && (
<div className="bg-blue-50 rounded-lg p-4 border border-blue-200">
<div className="flex items-center mb-2">
<Lightbulb className="w-5 h-5 text-blue-600 mr-2" />
<span className="font-medium text-blue-800"></span>
</div>
<p className="text-blue-700 text-sm leading-relaxed">{issue.suggestion}</p>
</div>
)}
{issue.ai_explanation && (
<div className="bg-green-50 rounded-lg p-4 border border-green-200">
<div className="flex items-center mb-2">
<Zap className="w-5 h-5 text-green-600 mr-2" />
<span className="font-medium text-green-800">AI </span>
</div>
<p className="text-green-700 text-sm leading-relaxed">{issue.ai_explanation}</p>
</div>
)}
</div>
</div>
);
if (issues.length === 0) {
return (
<div className="text-center py-16">
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6">
<CheckCircle className="w-12 h-12 text-green-600" />
</div>
<h3 className="text-2xl font-bold text-green-800 mb-3"></h3>
<p className="text-green-600 text-lg mb-6"></p>
<div className="bg-green-50 rounded-lg p-6 max-w-md mx-auto">
<p className="text-green-700 text-sm">
</p>
</div>
</div>
);
}
return (
<Tabs defaultValue="all" className="w-full">
<TabsList className="grid w-full grid-cols-5 mb-6">
<TabsTrigger value="all" className="text-sm">
({issues.length})
</TabsTrigger>
<TabsTrigger value="critical" className="text-sm">
({criticalIssues.length})
</TabsTrigger>
<TabsTrigger value="high" className="text-sm">
({highIssues.length})
</TabsTrigger>
<TabsTrigger value="medium" className="text-sm">
({mediumIssues.length})
</TabsTrigger>
<TabsTrigger value="low" className="text-sm">
({lowIssues.length})
</TabsTrigger>
</TabsList>
<TabsContent value="all" className="space-y-4 mt-6">
{issues.map((issue, index) => renderIssue(issue, index))}
</TabsContent>
<TabsContent value="critical" className="space-y-4 mt-6">
{criticalIssues.length > 0 ? (
criticalIssues.map((issue, index) => renderIssue(issue, index))
) : (
<div className="text-center py-12">
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2"></h3>
<p className="text-gray-500"></p>
</div>
)}
</TabsContent>
<TabsContent value="high" className="space-y-4 mt-6">
{highIssues.length > 0 ? (
highIssues.map((issue, index) => renderIssue(issue, index))
) : (
<div className="text-center py-12">
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2"></h3>
<p className="text-gray-500"></p>
</div>
)}
</TabsContent>
<TabsContent value="medium" className="space-y-4 mt-6">
{mediumIssues.length > 0 ? (
mediumIssues.map((issue, index) => renderIssue(issue, index))
) : (
<div className="text-center py-12">
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2"></h3>
<p className="text-gray-500"></p>
</div>
)}
</TabsContent>
<TabsContent value="low" className="space-y-4 mt-6">
{lowIssues.length > 0 ? (
lowIssues.map((issue, index) => renderIssue(issue, index))
) : (
<div className="text-center py-12">
<CheckCircle className="w-16 h-16 text-green-500 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2"></h3>
<p className="text-gray-500"></p>
</div>
)}
</TabsContent>
</Tabs>
);
}
export default function TaskDetail() {
const { id } = useParams<{ id: string }>();
const [task, setTask] = useState<AuditTask | null>(null);
const [issues, setIssues] = useState<AuditIssue[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (id) {
loadTaskDetail();
}
}, [id]);
const loadTaskDetail = async () => {
if (!id) return;
try {
setLoading(true);
const [taskData, issuesData] = await Promise.all([
api.getAuditTaskById(id),
api.getAuditIssues(id)
]);
setTask(taskData);
setIssues(issuesData);
} catch (error) {
console.error('Failed to load task detail:', error);
toast.error("加载任务详情失败");
} finally {
setLoading(false);
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed': return 'bg-green-100 text-green-800';
case 'running': return 'bg-red-50 text-red-800';
case 'failed': return 'bg-red-100 text-red-900';
default: return 'bg-gray-100 text-gray-800';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'completed': return <CheckCircle className="w-4 h-4" />;
case 'running': return <Activity className="w-4 h-4" />;
case 'failed': return <AlertTriangle className="w-4 h-4" />;
default: return <Clock className="w-4 h-4" />;
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('zh-CN', {
year: 'numeric',
@ -106,52 +286,56 @@ export default function TaskDetail() {
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600"></div>
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary"></div>
</div>
);
}
if (!task) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<AlertTriangle className="w-16 h-16 text-red-500 mx-auto mb-4" />
<h2 className="text-2xl font-bold text-gray-900 mb-2"></h2>
<p className="text-gray-600 mb-4">ID是否正确</p>
<Link to="/audit-tasks">
<Button>
<div className="space-y-6 animate-fade-in">
<div className="flex items-center space-x-4">
<Link to="/tasks">
<Button variant="outline" size="sm">
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
</Link>
</div>
<Card className="card-modern">
<CardContent className="empty-state py-16">
<div className="empty-icon">
<AlertTriangle className="w-8 h-8 text-red-500" />
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2"></h3>
<p className="text-gray-500">ID是否正确</p>
</CardContent>
</Card>
</div>
);
}
const progressPercentage = Math.round((task.scanned_files / task.total_files) * 100);
return (
<div className="space-y-6">
<div className="space-y-6 animate-fade-in">
{/* 页面标题 */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Link to="/audit-tasks">
<Link to="/tasks">
<Button variant="outline" size="sm">
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
</Link>
<div>
<h1 className="text-3xl font-bold text-gray-900">
{task.task_type === 'repository' ? '仓库审计任务' : '即时分析任务'}
</h1>
<p className="text-gray-600 mt-1">
{task.project?.name || '未知项目'}
</p>
<h1 className="page-title"></h1>
<p className="page-subtitle">{task.project?.name || '未知项目'} - </p>
</div>
</div>
<div className="flex items-center space-x-3">
<Badge className={getStatusColor(task.status)} variant="outline">
<Badge className={getStatusColor(task.status)}>
{getStatusIcon(task.status)}
<span className="ml-2">
{task.status === 'completed' ? '已完成' :
@ -159,320 +343,209 @@ export default function TaskDetail() {
task.status === 'failed' ? '失败' : '等待中'}
</span>
</Badge>
{task.status === 'completed' && (
<Button size="sm" className="btn-primary">
<Download className="w-4 h-4 mr-2" />
</Button>
)}
</div>
</div>
{/* 任务概览 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Card>
<CardContent className="p-6">
<div className="flex items-center">
<FileText className="h-8 w-8 text-blue-600" />
<div className="ml-4">
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-2xl font-bold">{task.scanned_files}/{task.total_files}</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="stat-card">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div>
<p className="stat-label"></p>
<p className="stat-value text-xl">{progressPercentage}%</p>
<Progress value={progressPercentage} className="mt-2" />
</div>
<div className="stat-icon from-primary to-accent">
<Activity className="w-5 h-5 text-white" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center">
<Code className="h-8 w-8 text-green-600" />
<div className="ml-4">
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-2xl font-bold">{task.total_lines.toLocaleString()}</p>
<Card className="stat-card">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div>
<p className="stat-label"></p>
<p className="stat-value text-xl text-orange-600">{task.issues_count}</p>
</div>
<div className="stat-icon from-orange-500 to-orange-600">
<Bug className="w-5 h-5 text-white" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center">
<AlertTriangle className="h-8 w-8 text-orange-600" />
<div className="ml-4">
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-2xl font-bold">{task.issues_count}</p>
<Card className="stat-card">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div>
<p className="stat-label"></p>
<p className="stat-value text-xl text-primary">{task.quality_score.toFixed(1)}</p>
</div>
<div className="stat-icon from-emerald-500 to-emerald-600">
<TrendingUp className="w-5 h-5 text-white" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center">
<CheckCircle className="h-8 w-8 text-purple-600" />
<div className="ml-4">
<p className="text-sm font-medium text-muted-foreground"></p>
<p className="text-2xl font-bold">{task.quality_score.toFixed(1)}</p>
<Card className="stat-card">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div>
<p className="stat-label"></p>
<p className="stat-value text-xl">{task.total_lines.toLocaleString()}</p>
</div>
<div className="stat-icon from-purple-500 to-purple-600">
<FileText className="w-5 h-5 text-white" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* 主要内容 */}
<Tabs defaultValue="overview" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="issues"></TabsTrigger>
<TabsTrigger value="config"></TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 任务信息 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium"></span>
<Badge variant="outline">
{task.task_type === 'repository' ? '仓库审计' : '即时分析'}
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium"></span>
<span className="text-sm text-muted-foreground">
{formatDate(task.created_at)}
</span>
</div>
{task.started_at && (
<div className="flex items-center justify-between">
<span className="text-sm font-medium"></span>
<span className="text-sm text-muted-foreground">
{formatDate(task.started_at)}
</span>
</div>
)}
{task.completed_at && (
<div className="flex items-center justify-between">
<span className="text-sm font-medium"></span>
<span className="text-sm text-muted-foreground">
{formatDate(task.completed_at)}
</span>
</div>
)}
<div className="flex items-center justify-between">
<span className="text-sm font-medium"></span>
<span className="text-sm text-muted-foreground">
{task.creator?.full_name || task.creator?.phone || '未知'}
</span>
</div>
</div>
{/* 扫描进度 */}
{task.status === 'running' && (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span></span>
<span>{task.scanned_files}/{task.total_files}</span>
</div>
<Progress value={(task.scanned_files / task.total_files) * 100} />
</div>
)}
{/* 质量评分 */}
{task.status === 'completed' && (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span></span>
<span>{task.quality_score.toFixed(1)}/100</span>
</div>
<Progress value={task.quality_score} />
</div>
)}
</CardContent>
</Card>
{/* 问题统计 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
{issues.length > 0 ? (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="text-center p-4 bg-red-50 rounded-lg">
<p className="text-2xl font-bold text-red-600">
{issues.filter(i => i.severity === 'critical').length}
</p>
<p className="text-sm text-red-600"></p>
</div>
<div className="text-center p-4 bg-orange-50 rounded-lg">
<p className="text-2xl font-bold text-orange-600">
{issues.filter(i => i.severity === 'high').length}
</p>
<p className="text-sm text-orange-600"></p>
</div>
<div className="text-center p-4 bg-yellow-50 rounded-lg">
<p className="text-2xl font-bold text-yellow-600">
{issues.filter(i => i.severity === 'medium').length}
</p>
<p className="text-sm text-yellow-600"></p>
</div>
<div className="text-center p-4 bg-blue-50 rounded-lg">
<p className="text-2xl font-bold text-blue-600">
{issues.filter(i => i.severity === 'low').length}
</p>
<p className="text-sm text-blue-600"></p>
</div>
</div>
{/* 问题类型分布 */}
<div className="space-y-2">
<h4 className="font-medium"></h4>
{['security', 'bug', 'performance', 'style', 'maintainability'].map(type => {
const count = issues.filter(i => i.issue_type === type).length;
const percentage = issues.length > 0 ? (count / issues.length) * 100 : 0;
return (
<div key={type} className="flex items-center justify-between">
<div className="flex items-center space-x-2">
{getTypeIcon(type)}
<span className="text-sm capitalize">{type}</span>
</div>
<div className="flex items-center space-x-2">
<div className="w-20 bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${percentage}%` }}
></div>
</div>
<span className="text-sm text-muted-foreground w-8">{count}</span>
</div>
</div>
);
})}
</div>
</div>
) : (
<div className="text-center py-8">
<CheckCircle className="w-12 h-12 text-green-600 mx-auto mb-4" />
<p className="text-green-600 font-medium"></p>
<p className="text-sm text-muted-foreground"></p>
</div>
)}
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="issues" className="space-y-6">
{issues.length > 0 ? (
<div className="space-y-4">
{issues.map((issue, index) => (
<Card key={issue.id} className="border-l-4 border-l-blue-500">
<CardContent className="p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center space-x-3">
{getTypeIcon(issue.issue_type)}
<div>
<h4 className="font-medium text-lg">{issue.title}</h4>
<p className="text-sm text-muted-foreground">
{issue.file_path}:{issue.line_number}
</p>
</div>
</div>
<Badge className={getSeverityColor(issue.severity)}>
{issue.severity}
</Badge>
</div>
<p className="text-sm text-muted-foreground mb-4">
{issue.description}
</p>
{issue.code_snippet && (
<div className="bg-gray-50 rounded p-3 mb-4">
<p className="text-sm font-medium mb-2"></p>
<pre className="text-xs bg-gray-100 p-2 rounded overflow-x-auto">
{issue.code_snippet}
</pre>
</div>
)}
{issue.suggestion && (
<div className="bg-blue-50 rounded p-3 mb-4">
<p className="text-sm font-medium text-blue-800 mb-1">
<Lightbulb className="w-4 h-4 inline mr-1" />
</p>
<p className="text-sm text-blue-700">{issue.suggestion}</p>
</div>
)}
{issue.ai_explanation && (
<div className="bg-green-50 rounded p-3">
<p className="text-sm font-medium text-green-800 mb-1">
AI
</p>
<p className="text-sm text-green-700">{issue.ai_explanation}</p>
</div>
)}
</CardContent>
</Card>
))}
</div>
) : (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<CheckCircle className="w-16 h-16 text-green-600 mx-auto mb-4" />
<h3 className="text-lg font-medium text-green-800 mb-2"></h3>
<p className="text-green-600"></p>
</CardContent>
</Card>
)}
</TabsContent>
<TabsContent value="config" className="space-y-6">
<Card>
{/* 任务信息 */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<Card className="card-modern">
<CardHeader>
<CardTitle></CardTitle>
<CardTitle className="flex items-center space-x-2">
<Shield className="w-5 h-5 text-primary" />
<span></span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="font-medium mb-2"></h4>
<p className="text-sm text-muted-foreground">
<p className="text-sm font-medium text-gray-500"></p>
<p className="text-base">
{task.task_type === 'repository' ? '仓库审计任务' : '即时分析任务'}
</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p className="text-base flex items-center">
<GitBranch className="w-4 h-4 mr-1" />
{task.branch_name || '默认分支'}
</p>
</div>
<div>
<h4 className="font-medium mb-2"></h4>
<div className="space-y-1">
{JSON.parse(task.exclude_patterns || '[]').length > 0 ? (
JSON.parse(task.exclude_patterns).map((pattern: string, index: number) => (
<Badge key={index} variant="outline" className="text-xs">
{pattern}
</Badge>
))
) : (
<p className="text-sm text-muted-foreground"></p>
)}
<p className="text-sm font-medium text-gray-500"></p>
<p className="text-base flex items-center">
<Calendar className="w-4 h-4 mr-1" />
{formatDate(task.created_at)}
</p>
</div>
{task.completed_at && (
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p className="text-base flex items-center">
<CheckCircle className="w-4 h-4 mr-1" />
{formatDate(task.completed_at)}
</p>
</div>
)}
</div>
{/* 排除模式 */}
{task.exclude_patterns && (
<div>
<p className="text-sm font-medium text-gray-500 mb-2"></p>
<div className="flex flex-wrap gap-2">
{JSON.parse(task.exclude_patterns).map((pattern: string) => (
<Badge key={pattern} variant="outline" className="text-xs">
{pattern}
</Badge>
))}
</div>
</div>
</div>
<div>
<h4 className="font-medium mb-2"></h4>
<pre className="text-xs bg-gray-100 p-3 rounded overflow-x-auto">
{JSON.stringify(JSON.parse(task.scan_config || '{}'), null, 2)}
</pre>
</div>
)}
{/* 扫描配置 */}
{task.scan_config && (
<div>
<p className="text-sm font-medium text-gray-500 mb-2"></p>
<div className="bg-gray-50 rounded-lg p-3">
<pre className="text-xs text-gray-600">
{JSON.stringify(JSON.parse(task.scan_config), null, 2)}
</pre>
</div>
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
<div>
<Card className="card-modern">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<FileText className="w-5 h-5 text-primary" />
<span></span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{task.project ? (
<>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<Link to={`/projects/${task.project.id}`} className="text-base text-primary hover:underline">
{task.project.name}
</Link>
</div>
{task.project.description && (
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p className="text-sm text-gray-600">{task.project.description}</p>
</div>
)}
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p className="text-base">{task.project.repository_type?.toUpperCase() || 'OTHER'}</p>
</div>
{task.project.programming_languages && (
<div>
<p className="text-sm font-medium text-gray-500 mb-2"></p>
<div className="flex flex-wrap gap-1">
{JSON.parse(task.project.programming_languages).map((lang: string) => (
<Badge key={lang} variant="secondary" className="text-xs">
{lang}
</Badge>
))}
</div>
</div>
)}
</>
) : (
<p className="text-gray-500"></p>
)}
</CardContent>
</Card>
</div>
</div>
{/* 问题列表 */}
{issues.length > 0 && (
<Card className="card-modern">
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Bug className="w-6 h-6 text-orange-600" />
<span> ({issues.length})</span>
</CardTitle>
</CardHeader>
<CardContent>
<IssuesList issues={issues} />
</CardContent>
</Card>
)}
</div>
);
}