init. project
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
Download,
|
||||
FileCode,
|
||||
FileText,
|
||||
Loader2,
|
||||
Play,
|
||||
Upload,
|
||||
} from "lucide-react";
|
||||
import DashboardLayout from "@/components/layout/dashboard-layout";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import {
|
||||
ConsistencyReport,
|
||||
downloadJson,
|
||||
loadConsistencyDraft,
|
||||
mockAnalyzeConsistency,
|
||||
saveConsistencyDraft,
|
||||
} from "@/lib/document-mock";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const severityVariant: Record<string, "default" | "secondary" | "destructive"> =
|
||||
{
|
||||
高: "destructive",
|
||||
中: "default",
|
||||
低: "secondary",
|
||||
};
|
||||
|
||||
export default function ConsistencyAnalysisPage() {
|
||||
const [requirementFile, setRequirementFile] = useState<File | null>(null);
|
||||
const [codeFile, setCodeFile] = useState<File | null>(null);
|
||||
const [report, setReport] = useState<ConsistencyReport | null>(null);
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
const draft = loadConsistencyDraft();
|
||||
if (!draft) {
|
||||
return;
|
||||
}
|
||||
setReport(draft);
|
||||
}, []);
|
||||
|
||||
const requirementDropzone = useDropzone({
|
||||
onDrop: useCallback((acceptedFiles: File[]) => {
|
||||
const file = acceptedFiles[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
setRequirementFile(file);
|
||||
}, []),
|
||||
maxFiles: 1,
|
||||
accept: {
|
||||
"application/pdf": [".pdf"],
|
||||
"application/msword": [".doc"],
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [
|
||||
".docx",
|
||||
],
|
||||
"application/json": [".json"],
|
||||
"text/plain": [".txt", ".md"],
|
||||
},
|
||||
});
|
||||
|
||||
const codeDropzone = useDropzone({
|
||||
onDrop: useCallback((acceptedFiles: File[]) => {
|
||||
const file = acceptedFiles[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
setCodeFile(file);
|
||||
}, []),
|
||||
maxFiles: 1,
|
||||
accept: {
|
||||
"text/plain": [
|
||||
".py",
|
||||
".js",
|
||||
".ts",
|
||||
".tsx",
|
||||
".java",
|
||||
".go",
|
||||
".cs",
|
||||
".cpp",
|
||||
".c",
|
||||
".md",
|
||||
".json",
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const handleAnalyze = async () => {
|
||||
if (!requirementFile || !codeFile) {
|
||||
toast({
|
||||
title: "缺少输入文件",
|
||||
description: "请同时上传需求文档和代码文件。",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsAnalyzing(true);
|
||||
try {
|
||||
const result = await mockAnalyzeConsistency(requirementFile, codeFile);
|
||||
setReport(result);
|
||||
saveConsistencyDraft(result);
|
||||
toast({
|
||||
title: "分析完成",
|
||||
description: `发现 ${result.issues.length} 项待处理问题。`,
|
||||
});
|
||||
} catch {
|
||||
toast({
|
||||
title: "分析失败",
|
||||
description: "前端模拟分析失败,请重试。",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsAnalyzing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportReport = () => {
|
||||
if (!report) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
downloadJson(`consistency-report-${timestamp}.json`, report);
|
||||
toast({
|
||||
title: "导出成功",
|
||||
description: "一致性分析报告已下载。",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">一致性分析</CardTitle>
|
||||
<CardDescription>
|
||||
上传需求文档与代码文件,比较需求-代码一致性并输出问题清单。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 lg:grid-cols-2">
|
||||
<div
|
||||
{...requirementDropzone.getRootProps()}
|
||||
className={cn(
|
||||
"rounded-lg border-2 border-dashed p-6 transition-colors",
|
||||
requirementDropzone.isDragActive
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-primary/40"
|
||||
)}
|
||||
>
|
||||
<input {...requirementDropzone.getInputProps()} />
|
||||
<div className="flex items-start gap-3">
|
||||
<FileText className="h-5 w-5 mt-0.5 text-muted-foreground" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">需求文档</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
支持 Word、PDF、JSON、TXT/MD
|
||||
</p>
|
||||
{requirementFile && (
|
||||
<p className="text-xs mt-3 inline-flex items-center gap-1 rounded-full border px-3 py-1">
|
||||
<Upload className="h-3.5 w-3.5" />
|
||||
{requirementFile.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
{...codeDropzone.getRootProps()}
|
||||
className={cn(
|
||||
"rounded-lg border-2 border-dashed p-6 transition-colors",
|
||||
codeDropzone.isDragActive
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-primary/40"
|
||||
)}
|
||||
>
|
||||
<input {...codeDropzone.getInputProps()} />
|
||||
<div className="flex items-start gap-3">
|
||||
<FileCode className="h-5 w-5 mt-0.5 text-muted-foreground" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">代码文件</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
支持 py/js/ts/tsx/java/go/cs/cpp/c 等文本代码文件
|
||||
</p>
|
||||
{codeFile && (
|
||||
<p className="text-xs mt-3 inline-flex items-center gap-1 rounded-full border px-3 py-1">
|
||||
<Upload className="h-3.5 w-3.5" />
|
||||
{codeFile.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2 flex flex-wrap gap-3">
|
||||
<Button onClick={handleAnalyze} disabled={isAnalyzing}>
|
||||
{isAnalyzing ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
开始分析
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleExportReport} disabled={!report}>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
导出分析 JSON
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{report && (
|
||||
<>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-sm text-muted-foreground">总体一致性</p>
|
||||
<p className="mt-2 text-3xl font-bold">{report.consistencyScore}%</p>
|
||||
<Progress value={report.consistencyScore} className="mt-3" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-sm text-muted-foreground">需求覆盖率</p>
|
||||
<p className="mt-2 text-3xl font-bold">{report.coverage}%</p>
|
||||
<Progress value={report.coverage} className="mt-3" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-sm text-muted-foreground">问题数量</p>
|
||||
<p className="mt-2 text-3xl font-bold">{report.issues.length}</p>
|
||||
<p className="mt-3 text-xs text-muted-foreground inline-flex items-center gap-1">
|
||||
<AlertTriangle className="h-3.5 w-3.5" />
|
||||
需优先处理高严重度问题
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">问题清单</CardTitle>
|
||||
<CardDescription>
|
||||
来源: {report.requirementFileName} vs {report.codeFileName}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3 max-h-[520px] overflow-y-auto pr-1">
|
||||
{report.issues.map((issue) => (
|
||||
<div
|
||||
key={issue.id}
|
||||
className="rounded-lg border p-4 space-y-3 bg-card"
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 text-primary" />
|
||||
<p className="text-sm font-medium">
|
||||
{issue.id} · {issue.type}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={severityVariant[issue.severity]}>
|
||||
严重度 {issue.severity}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground">{issue.summary}</p>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="rounded-md border p-2 text-xs text-muted-foreground">
|
||||
关联需求: {issue.requirementRef}
|
||||
</div>
|
||||
<div className="rounded-md border p-2 text-xs text-muted-foreground">
|
||||
关联代码: {issue.codeRef}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border border-primary/30 bg-primary/5 p-3 text-sm">
|
||||
建议处理: {issue.suggestion}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user