Files
AIclinicalresearch/frontend-v2/src/modules/rvw/components/MethodologyReport.tsx
HaHafeng 440f75255e 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
2026-01-10 22:52:15 +08:00

207 lines
10 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.
/**
* 方法学评估报告组件 - 专业版
*/
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>
);
}