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:
2026-01-10 22:52:15 +08:00
parent 179afa2c6b
commit 440f75255e
237 changed files with 3942 additions and 657 deletions

View 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>
);
}