feat(rvw): Complete RVW module development Phase 1-3

Summary:
- Migrate backend to modules/rvw with v2 API routes (/api/v2/rvw)
- Add new database fields: selectedAgents, editorialScore, methodologyStatus, picoExtract, isArchived
- Create frontend module in frontend-v2/src/modules/rvw
- Implement Dashboard with task list, filtering, batch operations
- Implement ReportDetail with dual tabs (editorial/methodology)
- Implement AgentModal for intelligent agent selection
- Register RVW module in moduleRegistry.ts
- Add navigation entry in TopNavigation
- Update documentation for RVW module status (v3.0)
- Update system status document (v2.9)

Features:
- User can select agents: editorial, methodology, or both
- Support batch task execution
- Task status filtering
- Replace console.log with logger service
- Maintain v1 API backward compatibility

Tested: Frontend and backend verified locally
Status: 85% complete (Phase 1-3 done)
This commit is contained in:
2026-01-07 22:39:08 +08:00
parent 06028c6952
commit 179afa2c6b
226 changed files with 5860 additions and 21 deletions

View File

@@ -0,0 +1,108 @@
/**
* 规范性评估报告组件
*/
import { AlertTriangle, CheckCircle, XCircle } from 'lucide-react';
import type { EditorialReviewResult } from '../types';
import ScoreRing from './ScoreRing';
interface EditorialReportProps {
data: EditorialReviewResult;
}
export default function EditorialReport({ data }: EditorialReportProps) {
const getStatusIcon = (status: 'pass' | 'warning' | 'fail') => {
switch (status) {
case 'pass':
return <CheckCircle className="w-5 h-5 text-green-500" />;
case 'warning':
return <AlertTriangle className="w-5 h-5 text-amber-500" />;
case 'fail':
return <XCircle className="w-5 h-5 text-red-500" />;
}
};
const getStatusTag = (status: 'pass' | 'warning' | 'fail') => {
switch (status) {
case 'pass':
return <span className="tag tag-green"></span>;
case 'warning':
return <span className="tag tag-amber"></span>;
case 'fail':
return <span className="tag tag-red"></span>;
}
};
const getItemBgClass = (status: 'pass' | 'warning' | 'fail') => {
switch (status) {
case 'pass':
return '';
case 'warning':
return 'bg-amber-50/50';
case 'fail':
return 'bg-red-50/50';
}
};
return (
<div className="space-y-6 fade-in">
{/* 总分卡片 */}
<div className={`bg-white p-6 rounded-2xl shadow-sm border flex items-center gap-8 ${
data.overall_score >= 80 ? 'border-green-200' :
data.overall_score >= 60 ? 'border-amber-200' : 'border-red-200'
}`}>
<ScoreRing score={data.overall_score} size="medium" />
<div className="flex-1">
<h3 className="font-bold text-lg text-slate-800">
{data.overall_score >= 80 ? '基本符合稿约规范' :
data.overall_score >= 60 ? '部分符合稿约规范' : '不符合稿约规范'}
</h3>
<p className="text-slate-600 text-sm mt-1">{data.summary}</p>
</div>
</div>
{/* 检测详情 */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div className="p-4 bg-slate-50 border-b border-gray-200 font-bold text-sm text-slate-700">
{data.items.length}
</div>
<div className="divide-y divide-gray-100">
{data.items.map((item, index) => (
<div key={index} className={`p-5 ${getItemBgClass(item.status)}`}>
<div className="flex gap-4">
<div className="mt-1">{getStatusIcon(item.status)}</div>
<div className="flex-1">
<div className="flex justify-between items-start">
<h4 className="font-bold text-sm text-slate-800">{item.criterion}</h4>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-500">{item.score}</span>
{getStatusTag(item.status)}
</div>
</div>
{item.issues && item.issues.length > 0 && (
<div className="mt-3 space-y-1">
{item.issues.map((issue, i) => (
<p key={i} className="text-sm text-slate-600"> {issue}</p>
))}
</div>
)}
{item.suggestions && item.suggestions.length > 0 && (
<div className="mt-3 bg-white border border-gray-200 p-3 rounded-lg">
<p className="text-xs font-bold text-slate-500 mb-1"></p>
{item.suggestions.map((suggestion, i) => (
<p key={i} className="text-xs text-slate-600"> {suggestion}</p>
))}
</div>
)}
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}