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

@@ -85,5 +85,6 @@ vite.config.*.timestamp-*

View File

@@ -52,5 +52,6 @@ exec nginx -g 'daemon off;'

View File

@@ -208,5 +208,6 @@ http {

View File

@@ -137,4 +137,3 @@ const TopNavigation = () => {
}
export default TopNavigation

View File

@@ -6,7 +6,8 @@ import {
FolderOpenOutlined,
ClearOutlined,
BarChartOutlined,
LineChartOutlined
LineChartOutlined,
AuditOutlined
} from '@ant-design/icons'
/**
@@ -78,6 +79,16 @@ export const MODULES: ModuleDefinition[] = [
description: '统计分析工具集Java团队开发',
isExternal: true, // 外部模块
},
{
id: 'review-system',
name: '预审稿',
path: '/rvw',
icon: AuditOutlined,
component: lazy(() => import('@/modules/rvw')),
placeholder: false, // RVW模块已开发
requiredVersion: 'basic',
description: '智能期刊审稿系统(稿约评审+方法学评审)',
},
]
/**

View File

@@ -555,5 +555,6 @@ export default FulltextDetailDrawer;

View File

@@ -148,5 +148,6 @@ export const useAssets = (activeTab: AssetTabType) => {

View File

@@ -138,5 +138,6 @@ export const useRecentTasks = () => {

View File

@@ -337,5 +337,6 @@ export default DropnaDialog;

View File

@@ -422,5 +422,6 @@ export default MetricTimePanel;

View File

@@ -308,5 +308,6 @@ export default PivotPanel;

View File

@@ -108,5 +108,6 @@ export function useSessionStatus({

View File

@@ -100,5 +100,6 @@ export interface DataStats {

View File

@@ -96,5 +96,6 @@ export type AssetTabType = 'all' | 'processed' | 'raw';

View File

@@ -217,3 +217,4 @@ export const documentSelectionApi = {

View File

@@ -285,3 +285,4 @@ export default KnowledgePage;

View File

@@ -223,3 +223,4 @@ export const useKnowledgeBaseStore = create<KnowledgeBaseState>((set, get) => ({

View File

@@ -40,3 +40,4 @@ export interface BatchTemplate {

View File

@@ -0,0 +1,502 @@
/**
* RVW - 预审稿模块入口
*
* @description 智能期刊审稿系统
* - 稿约评审:评估稿件是否符合期刊投稿要求
* - 方法学评审:评估临床研究的方法学质量
*
* @version Phase 3 - 前端重构
*/
import { useState, useEffect } from 'react'
import { useNavigate, Routes, Route, useParams } from 'react-router-dom'
import {
Upload,
Button,
Table,
Tag,
Space,
message,
Card,
Spin,
Modal,
Checkbox,
Progress,
Tabs,
Typography,
Tooltip,
Popconfirm
} from 'antd'
import {
UploadOutlined,
FileTextOutlined,
DeleteOutlined,
EyeOutlined,
PlayCircleOutlined,
ReloadOutlined,
CheckCircleOutlined,
ClockCircleOutlined,
ExclamationCircleOutlined,
DownloadOutlined
} from '@ant-design/icons'
import type { UploadFile, UploadProps } from 'antd'
const { Title, Text, Paragraph } = Typography
const { TabPane } = Tabs
// API 基础路径
const API_BASE = '/api/v2/rvw'
// 任务类型定义
interface ReviewTask {
id: string
fileName: string
status: 'pending' | 'processing' | 'completed' | 'failed'
selectedAgents: string[]
editorialScore?: number
methodologyStatus?: string
createdAt: string
updatedAt: string
editorialReview?: any
methodologyReview?: any
}
// 智能体选择弹窗
const AgentSelectModal: React.FC<{
visible: boolean
onCancel: () => void
onConfirm: (agents: string[]) => void
loading?: boolean
}> = ({ visible, onCancel, onConfirm, loading }) => {
const [selected, setSelected] = useState<string[]>(['editorial', 'methodology'])
return (
<Modal
title="选择审稿智能体"
open={visible}
onCancel={onCancel}
onOk={() => onConfirm(selected)}
confirmLoading={loading}
okText="开始审稿"
cancelText="取消"
>
<div className="py-4">
<Checkbox.Group
value={selected}
onChange={(vals) => setSelected(vals as string[])}
>
<Space direction="vertical" size="middle">
<Checkbox value="editorial">
<div>
<div className="font-medium">📝 稿</div>
<div className="text-gray-500 text-sm">稿稿11</div>
</div>
</Checkbox>
<Checkbox value="methodology">
<div>
<div className="font-medium">🔬 </div>
<div className="text-gray-500 text-sm">20</div>
</div>
</Checkbox>
</Space>
</Checkbox.Group>
{selected.length === 0 && (
<div className="text-red-500 mt-2"></div>
)}
</div>
</Modal>
)
}
// 任务列表页面
const TaskListPage: React.FC = () => {
const navigate = useNavigate()
const [tasks, setTasks] = useState<ReviewTask[]>([])
const [loading, setLoading] = useState(true)
const [uploading, setUploading] = useState(false)
const [showAgentModal, setShowAgentModal] = useState(false)
const [pendingFile, setPendingFile] = useState<File | null>(null)
const [runningTaskId, setRunningTaskId] = useState<string | null>(null)
// 加载任务列表
const loadTasks = async () => {
try {
setLoading(true)
const res = await fetch(`${API_BASE}/tasks`)
const data = await res.json()
if (data.success) {
setTasks(data.data || [])
}
} catch (error) {
message.error('加载任务列表失败')
} finally {
setLoading(false)
}
}
useEffect(() => {
loadTasks()
}, [])
// 上传文件
const handleUpload = async (agents: string[]) => {
if (!pendingFile || agents.length === 0) return
try {
setUploading(true)
const formData = new FormData()
formData.append('file', pendingFile)
formData.append('selectedAgents', JSON.stringify(agents))
const res = await fetch(`${API_BASE}/tasks`, {
method: 'POST',
body: formData,
})
const data = await res.json()
if (data.success) {
message.success('稿件上传成功,开始审稿...')
setShowAgentModal(false)
setPendingFile(null)
loadTasks()
} else {
message.error(data.error || '上传失败')
}
} catch (error) {
message.error('上传失败')
} finally {
setUploading(false)
}
}
// 删除任务
const handleDelete = async (taskId: string) => {
try {
const res = await fetch(`${API_BASE}/tasks/${taskId}`, {
method: 'DELETE',
})
const data = await res.json()
if (data.success) {
message.success('删除成功')
loadTasks()
}
} catch (error) {
message.error('删除失败')
}
}
// 运行审稿
const handleRun = async (taskId: string, agents: string[]) => {
try {
setRunningTaskId(taskId)
const res = await fetch(`${API_BASE}/tasks/${taskId}/run`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ selectedAgents: agents }),
})
const data = await res.json()
if (data.success) {
message.success('审稿任务已启动')
loadTasks()
}
} catch (error) {
message.error('启动审稿失败')
} finally {
setRunningTaskId(null)
}
}
// 上传配置
const uploadProps: UploadProps = {
beforeUpload: (file) => {
setPendingFile(file)
setShowAgentModal(true)
return false
},
showUploadList: false,
accept: '.pdf,.doc,.docx,.txt',
}
// 状态标签
const getStatusTag = (status: string) => {
const config: Record<string, { color: string; icon: React.ReactNode; text: string }> = {
pending: { color: 'default', icon: <ClockCircleOutlined />, text: '待审稿' },
processing: { color: 'processing', icon: <Spin size="small" />, text: '审稿中' },
completed: { color: 'success', icon: <CheckCircleOutlined />, text: '已完成' },
failed: { color: 'error', icon: <ExclamationCircleOutlined />, text: '失败' },
}
const c = config[status] || config.pending
return <Tag color={c.color} icon={c.icon}>{c.text}</Tag>
}
// 表格列
const columns = [
{
title: '文件名',
dataIndex: 'fileName',
key: 'fileName',
render: (name: string) => (
<Space>
<FileTextOutlined />
<span>{name}</span>
</Space>
),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 120,
render: (status: string) => getStatusTag(status),
},
{
title: '智能体',
dataIndex: 'selectedAgents',
key: 'selectedAgents',
width: 200,
render: (agents: string[]) => (
<Space>
{agents?.includes('editorial') && <Tag color="blue">稿</Tag>}
{agents?.includes('methodology') && <Tag color="purple"></Tag>}
</Space>
),
},
{
title: '稿约评分',
dataIndex: 'editorialScore',
key: 'editorialScore',
width: 100,
render: (score: number) => score ? `${score}` : '-',
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
render: (date: string) => new Date(date).toLocaleString('zh-CN'),
},
{
title: '操作',
key: 'actions',
width: 200,
render: (_: any, record: ReviewTask) => (
<Space>
{record.status === 'completed' && (
<Tooltip title="查看报告">
<Button
type="link"
icon={<EyeOutlined />}
onClick={() => navigate(`/rvw/report/${record.id}`)}
/>
</Tooltip>
)}
{record.status === 'pending' && (
<Tooltip title="开始审稿">
<Button
type="link"
icon={<PlayCircleOutlined />}
loading={runningTaskId === record.id}
onClick={() => handleRun(record.id, record.selectedAgents || ['editorial', 'methodology'])}
/>
</Tooltip>
)}
<Popconfirm
title="确定删除此任务?"
onConfirm={() => handleDelete(record.id)}
>
<Button type="link" danger icon={<DeleteOutlined />} />
</Popconfirm>
</Space>
),
},
]
return (
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<div>
<Title level={3} style={{ margin: 0 }}>📋 稿</Title>
<Text type="secondary">稿AI智能评估稿约符合度和方法学质量</Text>
</div>
<Space>
<Button icon={<ReloadOutlined />} onClick={loadTasks}></Button>
<Upload {...uploadProps}>
<Button type="primary" icon={<UploadOutlined />}>稿</Button>
</Upload>
</Space>
</div>
<Card>
<Table
columns={columns}
dataSource={tasks}
rowKey="id"
loading={loading}
pagination={{ pageSize: 10 }}
/>
</Card>
<AgentSelectModal
visible={showAgentModal}
onCancel={() => {
setShowAgentModal(false)
setPendingFile(null)
}}
onConfirm={handleUpload}
loading={uploading}
/>
</div>
)
}
// 报告详情页面
const ReportDetailPage: React.FC = () => {
const { taskId } = useParams<{ taskId: string }>()
const navigate = useNavigate()
const [task, setTask] = useState<ReviewTask | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const loadReport = async () => {
try {
const res = await fetch(`${API_BASE}/tasks/${taskId}/report`)
const data = await res.json()
if (data.success) {
setTask(data.data)
}
} catch (error) {
message.error('加载报告失败')
} finally {
setLoading(false)
}
}
if (taskId) loadReport()
}, [taskId])
if (loading) {
return (
<div className="flex justify-center items-center h-96">
<Spin size="large" tip="加载报告中..." />
</div>
)
}
if (!task) {
return (
<div className="p-6 text-center">
<Text type="secondary"></Text>
<br />
<Button onClick={() => navigate('/rvw')}></Button>
</div>
)
}
return (
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<div>
<Button onClick={() => navigate('/rvw')} className="mb-2"> </Button>
<Title level={3} style={{ margin: 0 }}>{task.fileName}</Title>
</div>
<Button icon={<DownloadOutlined />}></Button>
</div>
<Tabs defaultActiveKey="editorial">
{task.editorialReview && (
<TabPane tab="📝 稿约评审" key="editorial">
<Card>
<div className="mb-4">
<Title level={4}>{task.editorialScore || 0}</Title>
</div>
<div className="mb-4">
<Title level={5}></Title>
<Paragraph>{task.editorialReview.overallAssessment}</Paragraph>
</div>
<div className="mb-4">
<Title level={5}></Title>
{task.editorialReview.criteria?.map((item: any, index: number) => (
<Card key={index} size="small" className="mb-2">
<div className="flex justify-between">
<Text strong>{item.name}</Text>
<Tag color={item.passed ? 'success' : 'error'}>
{item.passed ? '通过' : '不通过'}
</Tag>
</div>
<Text type="secondary">{item.comment}</Text>
</Card>
))}
</div>
<div>
<Title level={5}></Title>
<ul>
{task.editorialReview.suggestions?.map((s: string, i: number) => (
<li key={i}>{s}</li>
))}
</ul>
</div>
</Card>
</TabPane>
)}
{task.methodologyReview && (
<TabPane tab="🔬 方法学评审" key="methodology">
<Card>
<div className="mb-4">
<Title level={4}>
<Tag color={task.methodologyReview.overallConclusion === 'acceptable' ? 'success' : 'warning'}>
{task.methodologyReview.overallConclusion === 'acceptable' ? '可接受' :
task.methodologyReview.overallConclusion === 'needs_revision' ? '需修改' : '不可接受'}
</Tag>
</Title>
</div>
<div className="mb-4">
<Title level={5}></Title>
<Paragraph>{task.methodologyReview.overallAssessment}</Paragraph>
</div>
<div className="mb-4">
<Title level={5}></Title>
{task.methodologyReview.checkpoints?.map((item: any, index: number) => (
<Card key={index} size="small" className="mb-2">
<div className="flex justify-between">
<Text strong>{item.name}</Text>
<Tag color={
item.status === 'pass' ? 'success' :
item.status === 'fail' ? 'error' : 'warning'
}>
{item.status === 'pass' ? '通过' :
item.status === 'fail' ? '不通过' : '部分通过'}
</Tag>
</div>
<Text type="secondary">{item.comment}</Text>
</Card>
))}
</div>
<div>
<Title level={5}></Title>
<ul>
{task.methodologyReview.recommendations?.map((r: string, i: number) => (
<li key={i}>{r}</li>
))}
</ul>
</div>
</Card>
</TabPane>
)}
</Tabs>
</div>
)
}
// 模块主入口
const RVWModule: React.FC = () => {
return (
<Routes>
<Route index element={<TaskListPage />} />
<Route path="report/:taskId" element={<ReportDetailPage />} />
</Routes>
)
}
export default RVWModule

View File

@@ -51,5 +51,6 @@ export { default as Placeholder } from './Placeholder';

View File

@@ -31,5 +31,6 @@ interface ImportMeta {