feat(rvw): Complete Phase 4-5 - Bug fixes and Word export
Summary: - Fix methodology score display issue in task list (show score instead of 'warn') - Add methodology_score field to database schema - Fix report display when only methodology agent is selected - Implement Word document export using docx library - Update documentation to v3.0/v3.1 Backend changes: - Add methodologyScore to Prisma schema and TaskSummary type - Update reviewWorker to save methodologyScore - Update getTaskList to return methodologyScore Frontend changes: - Install docx and file-saver libraries - Implement handleExportReport with Word generation - Fix activeTab auto-selection based on available data - Add proper imports for docx components Documentation: - Update RVW module status to 90% (Phase 1-5 complete) - Update system status document to v3.0 Tested: All review workflows verified, Word export functional
This commit is contained in:
206
frontend-v2/src/modules/rvw/components/MethodologyReport.tsx
Normal file
206
frontend-v2/src/modules/rvw/components/MethodologyReport.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 方法学评估报告组件 - 专业版
|
||||
*/
|
||||
import { XCircle, AlertTriangle, CheckCircle, Microscope, Lightbulb, MapPin, TrendingUp } from 'lucide-react';
|
||||
import type { MethodologyReviewResult } from '../types';
|
||||
|
||||
interface MethodologyReportProps {
|
||||
data: MethodologyReviewResult;
|
||||
}
|
||||
|
||||
export default function MethodologyReport({ data }: MethodologyReportProps) {
|
||||
// 统计问题数量
|
||||
const totalIssues = data.parts.reduce((sum, part) => sum + part.issues.length, 0);
|
||||
const majorIssues = data.parts.reduce((sum, part) => sum + part.issues.filter(i => i.severity === 'major').length, 0);
|
||||
const minorIssues = totalIssues - majorIssues;
|
||||
|
||||
const getSeverityStyle = (severity: 'major' | 'minor') => {
|
||||
return severity === 'major'
|
||||
? { icon: <XCircle className="w-4 h-4 text-red-500" />, label: '严重', badge: 'bg-red-100 text-red-700 border-red-200' }
|
||||
: { icon: <AlertTriangle className="w-4 h-4 text-amber-500" />, label: '轻微', badge: 'bg-amber-100 text-amber-700 border-amber-200' };
|
||||
};
|
||||
|
||||
const getScoreGrade = (score: number) => {
|
||||
if (score >= 90) return { label: '优秀', color: 'text-green-600', bg: 'bg-green-500' };
|
||||
if (score >= 80) return { label: '良好', color: 'text-green-600', bg: 'bg-green-500' };
|
||||
if (score >= 70) return { label: '中等', color: 'text-amber-600', bg: 'bg-amber-500' };
|
||||
if (score >= 60) return { label: '及格', color: 'text-amber-600', bg: 'bg-amber-500' };
|
||||
return { label: '不及格', color: 'text-red-600', bg: 'bg-red-500' };
|
||||
};
|
||||
|
||||
const getOverallStatus = () => {
|
||||
if (data.overall_score >= 80) return { label: '通过', color: 'text-green-700', bg: 'bg-green-50', border: 'border-green-200' };
|
||||
if (data.overall_score >= 60) return { label: '存疑', color: 'text-amber-700', bg: 'bg-amber-50', border: 'border-amber-200' };
|
||||
return { label: '不通过', color: 'text-red-700', bg: 'bg-red-50', border: 'border-red-200' };
|
||||
};
|
||||
|
||||
const grade = getScoreGrade(data.overall_score);
|
||||
const status = getOverallStatus();
|
||||
|
||||
return (
|
||||
<div className="space-y-6 fade-in">
|
||||
{/* 评分总览卡片 */}
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div className="p-6 bg-gradient-to-r from-slate-50 to-white">
|
||||
<div className="flex items-start gap-8">
|
||||
{/* 分数环 */}
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`w-24 h-24 rounded-full border-4 ${grade.bg.replace('bg-', 'border-')} flex items-center justify-center bg-white shadow-lg`}>
|
||||
<div className="text-center">
|
||||
<span className={`text-3xl font-bold ${grade.color}`}>{data.overall_score}</span>
|
||||
<span className="text-xs text-slate-400 block">分</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`mt-2 px-3 py-1 rounded-full text-xs font-bold ${grade.bg} text-white`}>
|
||||
{grade.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 评估摘要 */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Microscope className="w-5 h-5 text-purple-500" />
|
||||
<h3 className="font-bold text-lg text-slate-800">方法学评估</h3>
|
||||
<span className={`px-2.5 py-1 rounded-md text-xs font-bold ${status.bg} ${status.color} ${status.border} border`}>
|
||||
{status.label}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-slate-600 text-sm leading-relaxed mb-4">{data.summary}</p>
|
||||
|
||||
{/* 统计指标 */}
|
||||
<div className="flex gap-4">
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-slate-50 rounded-lg border border-slate-200">
|
||||
<span className="text-sm text-slate-600">共检测 <span className="font-bold text-slate-800">{data.parts.length}</span> 个方面</span>
|
||||
</div>
|
||||
{totalIssues === 0 ? (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-green-50 rounded-lg border border-green-100">
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
<span className="text-sm font-medium text-green-700">未发现问题</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{majorIssues > 0 && (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-red-50 rounded-lg border border-red-100">
|
||||
<XCircle className="w-4 h-4 text-red-500" />
|
||||
<span className="text-sm font-medium text-red-700">{majorIssues} 个严重问题</span>
|
||||
</div>
|
||||
)}
|
||||
{minorIssues > 0 && (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-amber-50 rounded-lg border border-amber-100">
|
||||
<AlertTriangle className="w-4 h-4 text-amber-500" />
|
||||
<span className="text-sm font-medium text-amber-700">{minorIssues} 个轻微问题</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分项详情标题 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<TrendingUp className="w-5 h-5 text-purple-500" />
|
||||
<h3 className="font-bold text-base text-slate-800">分项评估</h3>
|
||||
<span className="text-xs text-slate-400 bg-slate-100 px-2 py-0.5 rounded">共 {data.parts.length} 项</span>
|
||||
</div>
|
||||
|
||||
{/* 分项详情 */}
|
||||
<div className="space-y-4">
|
||||
{data.parts.map((part, partIndex) => (
|
||||
<div key={partIndex} className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
{/* 分项头部 */}
|
||||
<div className={`px-5 py-4 border-b ${part.issues.length === 0 ? 'bg-green-50/50 border-green-100' : 'bg-slate-50 border-slate-100'}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{part.issues.length === 0 ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
) : (
|
||||
<AlertTriangle className="w-5 h-5 text-amber-500" />
|
||||
)}
|
||||
<h4 className="font-semibold text-slate-800">{part.part}</h4>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`px-2.5 py-1 rounded-md text-xs font-bold ${
|
||||
part.score >= 80 ? 'bg-green-100 text-green-700' :
|
||||
part.score >= 60 ? 'bg-amber-100 text-amber-700' : 'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{part.score}分
|
||||
</span>
|
||||
{part.issues.length === 0 ? (
|
||||
<span className="px-2.5 py-1 rounded-md text-xs font-medium bg-green-100 text-green-700 flex items-center gap-1">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
无问题
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2.5 py-1 rounded-md text-xs font-medium bg-amber-100 text-amber-700">
|
||||
{part.issues.length} 个问题
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 问题列表 */}
|
||||
{part.issues.length > 0 && (
|
||||
<div className="divide-y divide-gray-50">
|
||||
{part.issues.map((issue, issueIndex) => {
|
||||
const severity = getSeverityStyle(issue.severity);
|
||||
return (
|
||||
<div key={issueIndex} className="px-5 py-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="mt-0.5">{severity.icon}</div>
|
||||
<div className="flex-1 space-y-3">
|
||||
{/* 问题标题和严重程度 */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-semibold text-slate-800">{issue.type}</span>
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium border ${severity.badge}`}>
|
||||
{severity.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 问题描述 */}
|
||||
<p className="text-sm text-slate-600 leading-relaxed">{issue.description}</p>
|
||||
|
||||
{/* 位置信息 */}
|
||||
{issue.location && (
|
||||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||||
<MapPin className="w-3.5 h-3.5" />
|
||||
<span>位置:{issue.location}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 改进建议 */}
|
||||
{issue.suggestion && (
|
||||
<div className="bg-indigo-50/50 rounded-lg p-3 border border-indigo-100">
|
||||
<div className="flex items-start gap-2">
|
||||
<Lightbulb className="w-4 h-4 text-indigo-500 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-indigo-600 mb-1">改进建议</p>
|
||||
<p className="text-sm text-slate-700">{issue.suggestion}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 无问题时的简洁显示 */}
|
||||
{part.issues.length === 0 && (
|
||||
<div className="px-5 py-4 text-sm text-green-600 flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
<span>该部分未发现方法学问题,符合规范要求</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user