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:
@@ -6,10 +6,15 @@ import ChatPage from './pages/ChatPage'
|
||||
import KnowledgePage from './pages/KnowledgePage'
|
||||
import HistoryPage from './pages/HistoryPage'
|
||||
import ReviewPage from './pages/ReviewPage'
|
||||
import { RvwDashboard } from './pages/rvw'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Routes>
|
||||
{/* RVW模块 - 独立布局 */}
|
||||
<Route path="/rvw" element={<RvwDashboard />} />
|
||||
|
||||
{/* 主系统 */}
|
||||
<Route path="/" element={<MainLayout />}>
|
||||
<Route index element={<HomePage />} />
|
||||
<Route path="chat" element={<ChatPage />} />
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
LogoutOutlined,
|
||||
FileSearchOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { ProjectSelector } from '../components/ProjectSelector'
|
||||
@@ -174,16 +175,32 @@ const MainLayout = () => {
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
width: 64,
|
||||
height: 64,
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
width: 64,
|
||||
height: 64,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 预审稿快捷入口 */}
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<FileSearchOutlined />}
|
||||
onClick={() => navigate('/rvw')}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
border: 'none',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
预审稿
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }} placement="bottomRight">
|
||||
<div style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
|
||||
236
frontend/src/pages/rvw/Dashboard.tsx
Normal file
236
frontend/src/pages/rvw/Dashboard.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* RVW审稿系统 - 主Dashboard页面
|
||||
*/
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { message } from 'antd';
|
||||
import {
|
||||
Sidebar,
|
||||
Header,
|
||||
FilterChips,
|
||||
TaskTable,
|
||||
BatchToolbar,
|
||||
AgentModal,
|
||||
ReportDetail,
|
||||
} from './components';
|
||||
import * as api from './api';
|
||||
import type { ReviewTask, ReviewReport, TaskFilters, AgentType } from './types';
|
||||
import './styles.css';
|
||||
|
||||
export default function Dashboard() {
|
||||
// ==================== State ====================
|
||||
const [currentView, setCurrentView] = useState<'dashboard' | 'archive'>('dashboard');
|
||||
const [tasks, setTasks] = useState<ReviewTask[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
const [filters, setFilters] = useState<TaskFilters>({ status: 'all', timeRange: 'all' });
|
||||
const [agentModalVisible, setAgentModalVisible] = useState(false);
|
||||
const [pendingTaskForRun, setPendingTaskForRun] = useState<ReviewTask | null>(null);
|
||||
|
||||
// 报告详情
|
||||
const [reportDetail, setReportDetail] = useState<ReviewReport | null>(null);
|
||||
|
||||
// ==================== 数据加载 ====================
|
||||
const loadTasks = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await api.getTasks(filters.status !== 'all' ? filters.status : undefined);
|
||||
|
||||
// 时间筛选
|
||||
let filtered = data;
|
||||
if (filters.timeRange === 'today') {
|
||||
const today = new Date().toDateString();
|
||||
filtered = data.filter(t => new Date(t.createdAt).toDateString() === today);
|
||||
} else if (filters.timeRange === 'week') {
|
||||
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
||||
filtered = data.filter(t => new Date(t.createdAt).getTime() > weekAgo);
|
||||
}
|
||||
|
||||
setTasks(filtered);
|
||||
} catch (error) {
|
||||
console.error('加载任务失败:', error);
|
||||
message.error('加载任务列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filters]);
|
||||
|
||||
useEffect(() => {
|
||||
loadTasks();
|
||||
}, [loadTasks]);
|
||||
|
||||
// 轮询更新进行中的任务
|
||||
useEffect(() => {
|
||||
const processingTasks = tasks.filter(t =>
|
||||
t.status === 'extracting' || t.status === 'reviewing'
|
||||
);
|
||||
|
||||
if (processingTasks.length === 0) return;
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
for (const task of processingTasks) {
|
||||
try {
|
||||
const updated = await api.getTask(task.id);
|
||||
setTasks(prev => prev.map(t => t.id === updated.id ? updated : t));
|
||||
} catch (error) {
|
||||
console.error('更新任务状态失败:', error);
|
||||
}
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [tasks]);
|
||||
|
||||
// ==================== 统计数据 ====================
|
||||
const counts = {
|
||||
all: tasks.length,
|
||||
pending: tasks.filter(t => t.status === 'pending').length,
|
||||
completed: tasks.filter(t => t.status === 'completed').length,
|
||||
};
|
||||
|
||||
// ==================== 事件处理 ====================
|
||||
const handleUpload = async (files: FileList) => {
|
||||
const uploadPromises = Array.from(files).map(async (file) => {
|
||||
try {
|
||||
message.loading({ content: `正在上传 ${file.name}...`, key: file.name });
|
||||
await api.uploadManuscript(file);
|
||||
message.success({ content: `${file.name} 上传成功`, key: file.name, duration: 2 });
|
||||
} catch (error: any) {
|
||||
message.error({ content: `${file.name} 上传失败: ${error.message}`, key: file.name, duration: 3 });
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(uploadPromises);
|
||||
loadTasks();
|
||||
};
|
||||
|
||||
const handleRunTask = (task: ReviewTask) => {
|
||||
setPendingTaskForRun(task);
|
||||
setAgentModalVisible(true);
|
||||
};
|
||||
|
||||
const handleRunBatch = () => {
|
||||
setPendingTaskForRun(null); // 批量模式
|
||||
setAgentModalVisible(true);
|
||||
};
|
||||
|
||||
const handleConfirmRun = async (agents: AgentType[]) => {
|
||||
try {
|
||||
if (pendingTaskForRun) {
|
||||
// 单个任务
|
||||
message.loading({ content: '正在启动审查...', key: 'run' });
|
||||
await api.runTask(pendingTaskForRun.id, agents);
|
||||
message.success({ content: '审查已启动', key: 'run', duration: 2 });
|
||||
} else {
|
||||
// 批量任务
|
||||
const pendingIds = selectedIds.filter(id => {
|
||||
const task = tasks.find(t => t.id === id);
|
||||
return task && task.status === 'pending';
|
||||
});
|
||||
|
||||
if (pendingIds.length === 0) {
|
||||
message.warning('没有待处理的任务');
|
||||
return;
|
||||
}
|
||||
|
||||
message.loading({ content: `正在启动 ${pendingIds.length} 个任务...`, key: 'run' });
|
||||
await api.batchRunTasks(pendingIds, agents);
|
||||
message.success({ content: `${pendingIds.length} 个任务已启动`, key: 'run', duration: 2 });
|
||||
setSelectedIds([]);
|
||||
}
|
||||
|
||||
loadTasks();
|
||||
} catch (error: any) {
|
||||
message.error({ content: error.message || '启动失败', key: 'run', duration: 3 });
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewReport = async (task: ReviewTask) => {
|
||||
try {
|
||||
message.loading({ content: '加载报告中...', key: 'report' });
|
||||
const report = await api.getTaskReport(task.id);
|
||||
setReportDetail(report);
|
||||
message.destroy('report');
|
||||
} catch (error: any) {
|
||||
message.error({ content: '加载报告失败', key: 'report', duration: 3 });
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackToList = () => {
|
||||
setReportDetail(null);
|
||||
};
|
||||
|
||||
// ==================== 渲染 ====================
|
||||
|
||||
// 报告详情视图
|
||||
if (reportDetail) {
|
||||
return (
|
||||
<div className="h-full flex overflow-hidden bg-slate-50">
|
||||
<Sidebar
|
||||
currentView={currentView}
|
||||
onViewChange={setCurrentView}
|
||||
/>
|
||||
<ReportDetail report={reportDetail} onBack={handleBackToList} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 主仪表盘视图
|
||||
return (
|
||||
<div className="h-full flex overflow-hidden bg-slate-50">
|
||||
<Sidebar
|
||||
currentView={currentView}
|
||||
onViewChange={setCurrentView}
|
||||
/>
|
||||
|
||||
<main className="flex-1 flex flex-col min-w-0 bg-white">
|
||||
<div className="flex-1 flex flex-col h-full relative fade-in">
|
||||
{/* 顶部操作区 */}
|
||||
<header className="bg-white px-8 pt-6 pb-4 border-b border-gray-100 flex-shrink-0 z-10">
|
||||
<Header onUpload={handleUpload} />
|
||||
<FilterChips
|
||||
filters={filters}
|
||||
counts={counts}
|
||||
onFilterChange={setFilters}
|
||||
/>
|
||||
</header>
|
||||
|
||||
{/* 列表区域 */}
|
||||
<div className="flex-1 overflow-auto bg-slate-50/50 p-6">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600" />
|
||||
</div>
|
||||
) : (
|
||||
<TaskTable
|
||||
tasks={tasks}
|
||||
selectedIds={selectedIds}
|
||||
onSelectChange={setSelectedIds}
|
||||
onViewReport={handleViewReport}
|
||||
onRunTask={handleRunTask}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 批量操作工具栏 */}
|
||||
<BatchToolbar
|
||||
selectedCount={selectedIds.length}
|
||||
onRunBatch={handleRunBatch}
|
||||
onClearSelection={() => setSelectedIds([])}
|
||||
/>
|
||||
|
||||
{/* 智能体选择弹窗 */}
|
||||
<AgentModal
|
||||
visible={agentModalVisible}
|
||||
taskCount={pendingTaskForRun ? 1 : selectedIds.length}
|
||||
onClose={() => {
|
||||
setAgentModalVisible(false);
|
||||
setPendingTaskForRun(null);
|
||||
}}
|
||||
onConfirm={handleConfirmRun}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
129
frontend/src/pages/rvw/api.ts
Normal file
129
frontend/src/pages/rvw/api.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* RVW模块API
|
||||
*/
|
||||
import axios from 'axios';
|
||||
import type { ReviewTask, ReviewReport, ApiResponse, AgentType } from './types';
|
||||
|
||||
const API_BASE = '/api/v2/rvw';
|
||||
|
||||
// 获取任务列表
|
||||
export async function getTasks(status?: string): Promise<ReviewTask[]> {
|
||||
const params = status && status !== 'all' ? { status } : {};
|
||||
const response = await axios.get<ApiResponse<ReviewTask[]>>(`${API_BASE}/tasks`, { params });
|
||||
return response.data.data || [];
|
||||
}
|
||||
|
||||
// 上传稿件
|
||||
export async function uploadManuscript(file: File, selectedAgents?: AgentType[]): Promise<{ taskId: string }> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
if (selectedAgents) {
|
||||
formData.append('selectedAgents', JSON.stringify(selectedAgents));
|
||||
}
|
||||
|
||||
const response = await axios.post<ApiResponse<{ taskId: string }>>(`${API_BASE}/tasks`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.error || '上传失败');
|
||||
}
|
||||
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
// 获取任务详情
|
||||
export async function getTask(taskId: string): Promise<ReviewTask> {
|
||||
const response = await axios.get<ApiResponse<ReviewTask>>(`${API_BASE}/tasks/${taskId}`);
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
// 获取任务报告
|
||||
export async function getTaskReport(taskId: string): Promise<ReviewReport> {
|
||||
const response = await axios.get<ApiResponse<ReviewReport>>(`${API_BASE}/tasks/${taskId}/report`);
|
||||
return response.data.data!;
|
||||
}
|
||||
|
||||
// 运行审查任务
|
||||
export async function runTask(taskId: string, agents: AgentType[]): Promise<void> {
|
||||
const response = await axios.post<ApiResponse<void>>(`${API_BASE}/tasks/${taskId}/run`, { agents });
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.error || '运行失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 批量运行审查任务
|
||||
export async function batchRunTasks(taskIds: string[], agents: AgentType[]): Promise<void> {
|
||||
const response = await axios.post<ApiResponse<void>>(`${API_BASE}/tasks/batch-run`, { taskIds, agents });
|
||||
if (!response.data.success) {
|
||||
throw new Error(response.data.error || '批量运行失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 删除任务
|
||||
export async function deleteTask(taskId: string): Promise<void> {
|
||||
await axios.delete(`${API_BASE}/tasks/${taskId}`);
|
||||
}
|
||||
|
||||
// 轮询任务状态
|
||||
export async function pollTaskUntilComplete(
|
||||
taskId: string,
|
||||
onUpdate?: (task: ReviewTask) => void,
|
||||
maxAttempts = 120,
|
||||
interval = 3000
|
||||
): Promise<ReviewTask> {
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
const task = await getTask(taskId);
|
||||
onUpdate?.(task);
|
||||
|
||||
if (task.status === 'completed' || task.status === 'failed') {
|
||||
return task;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, interval));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
throw new Error('任务超时');
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
// 格式化时长
|
||||
export function formatDuration(seconds: number): string {
|
||||
if (seconds < 60) return `${seconds}秒`;
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}分${secs}秒`;
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
export function formatTime(dateStr: string): string {
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
|
||||
// 小于1分钟
|
||||
if (diff < 60 * 1000) return '刚刚';
|
||||
|
||||
// 小于1小时
|
||||
if (diff < 60 * 60 * 1000) {
|
||||
return `${Math.floor(diff / (60 * 1000))}分钟前`;
|
||||
}
|
||||
|
||||
// 今天
|
||||
if (date.toDateString() === now.toDateString()) {
|
||||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
// 其他
|
||||
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' });
|
||||
}
|
||||
|
||||
123
frontend/src/pages/rvw/components/AgentModal.tsx
Normal file
123
frontend/src/pages/rvw/components/AgentModal.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 智能体选择弹窗
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { PlayCircle, X } from 'lucide-react';
|
||||
import type { AgentType } from '../types';
|
||||
|
||||
interface AgentModalProps {
|
||||
visible: boolean;
|
||||
taskCount: number;
|
||||
onClose: () => void;
|
||||
onConfirm: (agents: AgentType[]) => void;
|
||||
}
|
||||
|
||||
export default function AgentModal({ visible, taskCount, onClose, onConfirm }: AgentModalProps) {
|
||||
const [selectedAgents, setSelectedAgents] = useState<AgentType[]>(['editorial']);
|
||||
|
||||
const toggleAgent = (agent: AgentType) => {
|
||||
if (selectedAgents.includes(agent)) {
|
||||
// 至少保留一个
|
||||
if (selectedAgents.length > 1) {
|
||||
setSelectedAgents(selectedAgents.filter(a => a !== agent));
|
||||
}
|
||||
} else {
|
||||
setSelectedAgents([...selectedAgents, agent]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
onConfirm(selectedAgents);
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-slate-900/50 z-50 flex items-center justify-center backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-[400px] overflow-hidden transform transition-all scale-100 fade-in">
|
||||
{/* 头部 */}
|
||||
<div className="bg-slate-900 p-5 text-white flex items-center justify-between">
|
||||
<h3 className="font-bold text-lg flex items-center gap-2">
|
||||
<PlayCircle className="w-5 h-5 text-indigo-400" />
|
||||
发起智能审稿
|
||||
</h3>
|
||||
<button onClick={onClose} className="text-slate-400 hover:text-white transition-colors">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 内容 */}
|
||||
<div className="p-6 space-y-4">
|
||||
<p className="text-sm text-slate-600 mb-4">
|
||||
{taskCount > 1 ? `已选择 ${taskCount} 个稿件,请选择审稿维度:` : '请选择审稿维度:'}
|
||||
</p>
|
||||
|
||||
{/* 规范性智能体 */}
|
||||
<label
|
||||
className={`flex items-start gap-3 p-4 border rounded-xl cursor-pointer transition-all ${
|
||||
selectedAgents.includes('editorial')
|
||||
? 'border-indigo-500 bg-indigo-50'
|
||||
: 'border-gray-200 hover:border-indigo-300'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedAgents.includes('editorial')}
|
||||
onChange={() => toggleAgent('editorial')}
|
||||
className="mt-1 w-4 h-4 text-indigo-600 rounded border-gray-300 focus:ring-indigo-500"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<span className="block font-bold text-slate-800 text-sm">稿约规范性智能体</span>
|
||||
<span className="block text-xs text-slate-500 mt-0.5">
|
||||
检查格式、参考文献、图片、伦理声明等11项标准
|
||||
</span>
|
||||
</div>
|
||||
<span className="tag tag-blue">快速</span>
|
||||
</label>
|
||||
|
||||
{/* 方法学智能体 */}
|
||||
<label
|
||||
className={`flex items-start gap-3 p-4 border rounded-xl cursor-pointer transition-all ${
|
||||
selectedAgents.includes('methodology')
|
||||
? 'border-indigo-500 bg-indigo-50'
|
||||
: 'border-gray-200 hover:border-indigo-300'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedAgents.includes('methodology')}
|
||||
onChange={() => toggleAgent('methodology')}
|
||||
className="mt-1 w-4 h-4 text-indigo-600 rounded border-gray-300 focus:ring-indigo-500"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<span className="block font-bold text-slate-800 text-sm">方法学统计智能体</span>
|
||||
<span className="block text-xs text-slate-500 mt-0.5">
|
||||
深度推理检验方法、统计逻辑、研究设计等20项评估
|
||||
</span>
|
||||
</div>
|
||||
<span className="tag tag-purple">深度</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<div className="p-4 bg-slate-50 flex justify-end gap-3 border-t border-gray-100">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm text-slate-600 hover:bg-gray-200 rounded-lg transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
disabled={selectedAgents.length === 0}
|
||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-bold rounded-lg shadow-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
立即运行
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
43
frontend/src/pages/rvw/components/BatchToolbar.tsx
Normal file
43
frontend/src/pages/rvw/components/BatchToolbar.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 批量操作浮动工具栏
|
||||
*/
|
||||
import { Play, X } from 'lucide-react';
|
||||
|
||||
interface BatchToolbarProps {
|
||||
selectedCount: number;
|
||||
onRunBatch: () => void;
|
||||
onClearSelection: () => void;
|
||||
}
|
||||
|
||||
export default function BatchToolbar({ selectedCount, onRunBatch, onClearSelection }: BatchToolbarProps) {
|
||||
if (selectedCount === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-8 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white px-5 py-3 rounded-full shadow-2xl flex items-center gap-6 z-30 fade-in border border-slate-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-5 h-5 rounded-full bg-indigo-500 flex items-center justify-center text-[10px] font-bold">
|
||||
{selectedCount}
|
||||
</div>
|
||||
<span className="text-sm font-medium">个文件已选中</span>
|
||||
</div>
|
||||
|
||||
<div className="h-4 w-px bg-slate-600" />
|
||||
|
||||
<button
|
||||
onClick={onRunBatch}
|
||||
className="text-sm font-bold text-white hover:text-indigo-300 flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<Play className="w-4 h-4 text-green-400" />
|
||||
运行智能审稿
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onClearSelection}
|
||||
className="text-slate-400 hover:text-white ml-2"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
108
frontend/src/pages/rvw/components/EditorialReport.tsx
Normal file
108
frontend/src/pages/rvw/components/EditorialReport.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
66
frontend/src/pages/rvw/components/FilterChips.tsx
Normal file
66
frontend/src/pages/rvw/components/FilterChips.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 筛选Chips组件
|
||||
*/
|
||||
import type { TaskFilters } from '../types';
|
||||
|
||||
interface FilterChipsProps {
|
||||
filters: TaskFilters;
|
||||
counts: { all: number; pending: number; completed: number };
|
||||
onFilterChange: (filters: TaskFilters) => void;
|
||||
}
|
||||
|
||||
export default function FilterChips({ filters, counts, onFilterChange }: FilterChipsProps) {
|
||||
const statusOptions = [
|
||||
{ value: 'all' as const, label: '全部', count: counts.all },
|
||||
{ value: 'pending' as const, label: '待处理', count: counts.pending },
|
||||
{ value: 'completed' as const, label: '已完成', count: counts.completed },
|
||||
];
|
||||
|
||||
const timeOptions = [
|
||||
{ value: 'all' as const, label: '不限' },
|
||||
{ value: 'today' as const, label: '今天' },
|
||||
{ value: 'week' as const, label: '近7天' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
{/* 状态筛选 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider mr-2">状态:</span>
|
||||
{statusOptions.map(option => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => onFilterChange({ ...filters, status: option.value })}
|
||||
className={`filter-chip ${filters.status === option.value ? 'active' : ''}`}
|
||||
>
|
||||
{option.label}
|
||||
{option.count !== undefined && (
|
||||
<span className={`ml-1 text-xs px-1.5 rounded-full ${
|
||||
filters.status === option.value ? 'bg-black/10' : 'bg-slate-200'
|
||||
}`}>
|
||||
{option.count}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="h-4 w-px bg-gray-200 mx-2" />
|
||||
|
||||
{/* 时间筛选 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-bold text-slate-400 uppercase tracking-wider mr-2">时间:</span>
|
||||
{timeOptions.map(option => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => onFilterChange({ ...filters, timeRange: option.value })}
|
||||
className={`filter-chip ${filters.timeRange === option.value ? 'active' : ''}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
56
frontend/src/pages/rvw/components/Header.tsx
Normal file
56
frontend/src/pages/rvw/components/Header.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Dashboard头部组件
|
||||
*/
|
||||
import { useRef } from 'react';
|
||||
import { BrainCircuit, UploadCloud } from 'lucide-react';
|
||||
|
||||
interface HeaderProps {
|
||||
onUpload: (files: FileList) => void;
|
||||
}
|
||||
|
||||
export default function Header({ onUpload }: HeaderProps) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
onUpload(e.target.files);
|
||||
// 重置input以允许选择相同文件
|
||||
e.target.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
{/* Logo区域 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-indigo-50 p-2 rounded-lg text-indigo-700">
|
||||
<BrainCircuit className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-slate-800">智能审稿系统</h1>
|
||||
<p className="text-xs text-slate-500">当前工作区:编辑部初审组</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 上传按钮 */}
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept=".pdf,.doc,.docx"
|
||||
className="hidden"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="px-5 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-bold flex items-center gap-2 shadow-sm transition-all hover:-translate-y-0.5"
|
||||
>
|
||||
<UploadCloud className="w-4 h-4" />
|
||||
上传新稿件
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
113
frontend/src/pages/rvw/components/MethodologyReport.tsx
Normal file
113
frontend/src/pages/rvw/components/MethodologyReport.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 方法学评估报告组件
|
||||
*/
|
||||
import { XCircle, AlertTriangle, CheckCircle } from 'lucide-react';
|
||||
import type { MethodologyReviewResult } from '../types';
|
||||
import ScoreRing from './ScoreRing';
|
||||
|
||||
interface MethodologyReportProps {
|
||||
data: MethodologyReviewResult;
|
||||
}
|
||||
|
||||
export default function MethodologyReport({ data }: MethodologyReportProps) {
|
||||
const getSeverityStyle = (severity: 'major' | 'minor') => {
|
||||
return severity === 'major'
|
||||
? { border: 'border-red-200', bg: 'bg-red-50', icon: <XCircle className="w-4 h-4 text-red-500" />, label: '严重' }
|
||||
: { border: 'border-amber-200', bg: 'bg-amber-50', icon: <AlertTriangle className="w-4 h-4 text-amber-500" />, label: '轻微' };
|
||||
};
|
||||
|
||||
const getOverallStatus = () => {
|
||||
if (data.overall_score >= 80) return { label: '通过', color: 'text-green-700', bg: 'bg-green-50' };
|
||||
if (data.overall_score >= 60) return { label: '存疑', color: 'text-amber-700', bg: 'bg-amber-50' };
|
||||
return { label: '不通过', color: 'text-red-700', bg: 'bg-red-50' };
|
||||
};
|
||||
|
||||
const status = getOverallStatus();
|
||||
|
||||
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 flex items-center gap-2">
|
||||
方法学评估
|
||||
<span className={`text-sm px-2 py-0.5 rounded ${status.bg} ${status.color}`}>
|
||||
{status.label}
|
||||
</span>
|
||||
</h3>
|
||||
<p className="text-slate-600 text-sm mt-1">{data.summary}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分项详情 */}
|
||||
{data.parts.map((part, partIndex) => (
|
||||
<div key={partIndex} 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 flex justify-between items-center">
|
||||
<span className="font-bold text-sm text-slate-700">{part.part}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500">{part.score}分</span>
|
||||
{part.issues.length === 0 ? (
|
||||
<span className="tag tag-green flex items-center gap-1">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
无问题
|
||||
</span>
|
||||
) : (
|
||||
<span className="tag tag-amber">
|
||||
{part.issues.length}个问题
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{part.issues.length > 0 ? (
|
||||
<div className="divide-y divide-gray-100">
|
||||
{part.issues.map((issue, issueIndex) => {
|
||||
const severity = getSeverityStyle(issue.severity);
|
||||
return (
|
||||
<div key={issueIndex} className={`p-5 ${severity.bg}`}>
|
||||
<div className="flex gap-3">
|
||||
<div className="mt-0.5">{severity.icon}</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-bold text-sm text-slate-800">{issue.type}</span>
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded ${
|
||||
issue.severity === 'major' ? 'bg-red-100 text-red-700' : 'bg-amber-100 text-amber-700'
|
||||
}`}>
|
||||
{severity.label}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600">{issue.description}</p>
|
||||
{issue.location && (
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
位置:{issue.location}
|
||||
</p>
|
||||
)}
|
||||
{issue.suggestion && (
|
||||
<div className="mt-3 bg-white border border-gray-200 p-3 rounded-lg">
|
||||
<p className="text-xs text-slate-600">
|
||||
<strong>建议:</strong>{issue.suggestion}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-6 text-center text-slate-500 text-sm">
|
||||
<CheckCircle className="w-8 h-8 text-green-400 mx-auto mb-2" />
|
||||
该部分未发现问题
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
110
frontend/src/pages/rvw/components/ReportDetail.tsx
Normal file
110
frontend/src/pages/rvw/components/ReportDetail.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 报告详情页组件
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { ArrowLeft, FileCheck, Tag } from 'lucide-react';
|
||||
import type { ReviewReport } from '../types';
|
||||
import EditorialReport from './EditorialReport';
|
||||
import MethodologyReport from './MethodologyReport';
|
||||
|
||||
interface ReportDetailProps {
|
||||
report: ReviewReport;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export default function ReportDetail({ report, onBack }: ReportDetailProps) {
|
||||
const [activeTab, setActiveTab] = useState<'editorial' | 'methodology'>('editorial');
|
||||
|
||||
const hasEditorial = !!report.editorialReview;
|
||||
const hasMethodology = !!report.methodologyReview;
|
||||
|
||||
// 如果只有方法学,默认显示方法学
|
||||
const effectiveTab = activeTab === 'editorial' && !hasEditorial && hasMethodology ? 'methodology' : activeTab;
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col h-full bg-slate-50 relative fade-in">
|
||||
{/* 顶部导航栏 */}
|
||||
<header className="h-16 bg-white border-b border-gray-200 px-6 flex items-center justify-between sticky top-0 z-20 shadow-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-2 text-slate-500 hover:text-slate-800 transition-colors px-2 py-1 rounded hover:bg-slate-100"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
<span className="text-sm font-medium">返回列表</span>
|
||||
</button>
|
||||
<div className="h-6 w-px bg-slate-200" />
|
||||
<div>
|
||||
<h1 className="text-base font-bold text-slate-800 flex items-center gap-2">
|
||||
{report.fileName}
|
||||
{report.overallScore && (
|
||||
<span className={`tag ${
|
||||
report.overallScore >= 80 ? 'tag-green' :
|
||||
report.overallScore >= 60 ? 'tag-amber' : 'tag-red'
|
||||
}`}>
|
||||
{report.overallScore}分
|
||||
</span>
|
||||
)}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button className="px-3 py-1.5 bg-indigo-600 text-white rounded text-sm font-medium hover:bg-indigo-700 transition shadow-sm flex items-center gap-2">
|
||||
<FileCheck className="w-4 h-4" />
|
||||
导出报告
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="flex-1 overflow-auto p-8 max-w-5xl mx-auto w-full">
|
||||
{/* Tab切换 */}
|
||||
{(hasEditorial || hasMethodology) && (
|
||||
<div className="flex gap-1 bg-slate-200/50 p-1 rounded-lg mb-8 w-fit mx-auto">
|
||||
{hasEditorial && (
|
||||
<button
|
||||
onClick={() => setActiveTab('editorial')}
|
||||
className={`px-6 py-2 rounded-md text-sm transition-all ${
|
||||
effectiveTab === 'editorial'
|
||||
? 'font-bold bg-white text-indigo-600 shadow-sm'
|
||||
: 'font-medium text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
稿约规范性 ({report.editorialReview?.overall_score}分)
|
||||
</button>
|
||||
)}
|
||||
{hasMethodology && (
|
||||
<button
|
||||
onClick={() => setActiveTab('methodology')}
|
||||
className={`px-6 py-2 rounded-md text-sm transition-all ${
|
||||
effectiveTab === 'methodology'
|
||||
? 'font-bold bg-white text-indigo-600 shadow-sm'
|
||||
: 'font-medium text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
方法学评估 ({report.methodologyReview?.overall_score}分)
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 报告内容 */}
|
||||
{effectiveTab === 'editorial' && report.editorialReview && (
|
||||
<EditorialReport data={report.editorialReview} />
|
||||
)}
|
||||
{effectiveTab === 'methodology' && report.methodologyReview && (
|
||||
<MethodologyReport data={report.methodologyReview} />
|
||||
)}
|
||||
|
||||
{/* 无数据状态 */}
|
||||
{!hasEditorial && !hasMethodology && (
|
||||
<div className="text-center py-12 text-slate-500">
|
||||
<Tag className="w-12 h-12 mx-auto mb-4 text-slate-300" />
|
||||
<p>暂无评估报告</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
38
frontend/src/pages/rvw/components/ScoreRing.tsx
Normal file
38
frontend/src/pages/rvw/components/ScoreRing.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 评分环组件
|
||||
*/
|
||||
|
||||
interface ScoreRingProps {
|
||||
score: number;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
export default function ScoreRing({ score, size = 'medium', showLabel = true }: ScoreRingProps) {
|
||||
const sizeStyles = {
|
||||
small: 'w-12 h-12 text-lg border-4',
|
||||
medium: 'w-20 h-20 text-2xl border-6',
|
||||
large: 'w-24 h-24 text-3xl border-8',
|
||||
};
|
||||
|
||||
const getScoreStatus = (score: number) => {
|
||||
if (score >= 80) return { class: 'pass', label: 'Pass', bgColor: 'bg-green-50', borderColor: 'border-green-500', textColor: 'text-green-700' };
|
||||
if (score >= 60) return { class: 'warn', label: 'Warning', bgColor: 'bg-amber-50', borderColor: 'border-amber-500', textColor: 'text-amber-700' };
|
||||
return { class: 'fail', label: 'Fail', bgColor: 'bg-red-50', borderColor: 'border-red-500', textColor: 'text-red-700' };
|
||||
};
|
||||
|
||||
const status = getScoreStatus(score);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`rounded-full flex flex-col items-center justify-center ${sizeStyles[size]} ${status.bgColor} ${status.borderColor} ${status.textColor}`}
|
||||
style={{ borderWidth: size === 'small' ? 4 : size === 'medium' ? 6 : 8 }}
|
||||
>
|
||||
<span className="font-bold">{score}</span>
|
||||
{showLabel && size !== 'small' && (
|
||||
<span className="text-[10px] font-bold uppercase">{status.label}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
73
frontend/src/pages/rvw/components/Sidebar.tsx
Normal file
73
frontend/src/pages/rvw/components/Sidebar.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* RVW侧边栏组件
|
||||
*/
|
||||
import { LayoutGrid, Archive, Settings, BrainCircuit } from 'lucide-react';
|
||||
|
||||
interface SidebarProps {
|
||||
currentView: 'dashboard' | 'archive';
|
||||
onViewChange: (view: 'dashboard' | 'archive') => void;
|
||||
onSettingsClick?: () => void;
|
||||
}
|
||||
|
||||
export default function Sidebar({ currentView, onViewChange, onSettingsClick }: SidebarProps) {
|
||||
return (
|
||||
<aside className="w-[72px] bg-slate-900 flex flex-col items-center py-6 gap-4 z-20 shadow-xl flex-shrink-0 relative">
|
||||
{/* Logo */}
|
||||
<div
|
||||
className="w-10 h-10 bg-indigo-500 rounded-xl flex items-center justify-center text-white shadow-lg mb-4"
|
||||
title="智能审稿系统"
|
||||
>
|
||||
<BrainCircuit className="w-6 h-6" />
|
||||
</div>
|
||||
|
||||
{/* 审稿工作台 */}
|
||||
<button
|
||||
onClick={() => onViewChange('dashboard')}
|
||||
className={`sidebar-btn w-10 h-10 rounded-lg flex items-center justify-center transition-colors relative group
|
||||
${currentView === 'dashboard'
|
||||
? 'bg-white/10 text-white'
|
||||
: 'text-slate-400 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
title="审稿工作台"
|
||||
>
|
||||
<LayoutGrid className="w-5 h-5" />
|
||||
<span className="sidebar-tooltip">审稿工作台</span>
|
||||
</button>
|
||||
|
||||
{/* 历史归档 */}
|
||||
<button
|
||||
onClick={() => onViewChange('archive')}
|
||||
className={`sidebar-btn w-10 h-10 rounded-lg flex items-center justify-center transition-colors relative group
|
||||
${currentView === 'archive'
|
||||
? 'bg-white/10 text-white'
|
||||
: 'text-slate-400 hover:bg-white/10 hover:text-white'
|
||||
}`}
|
||||
title="历史归档"
|
||||
>
|
||||
<Archive className="w-5 h-5" />
|
||||
<span className="sidebar-tooltip">历史归档</span>
|
||||
</button>
|
||||
|
||||
{/* 底部区域 */}
|
||||
<div className="mt-auto flex flex-col gap-4 relative">
|
||||
{/* 系统设置 */}
|
||||
<button
|
||||
onClick={onSettingsClick}
|
||||
className="sidebar-btn w-10 h-10 rounded-lg text-slate-400 flex items-center justify-center hover:bg-white/10 hover:text-white transition-colors relative group"
|
||||
title="系统设置"
|
||||
>
|
||||
<Settings className="w-5 h-5" />
|
||||
<span className="sidebar-tooltip">系统设置</span>
|
||||
</button>
|
||||
|
||||
{/* 用户头像 */}
|
||||
<div className="relative">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-tr from-indigo-500 to-purple-500 flex items-center justify-center text-xs font-bold text-white border-2 border-slate-700 cursor-pointer hover:border-white transition-all shadow-md">
|
||||
编辑
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
252
frontend/src/pages/rvw/components/TaskTable.tsx
Normal file
252
frontend/src/pages/rvw/components/TaskTable.tsx
Normal file
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* 任务表格组件
|
||||
*/
|
||||
import { FileText, FileType2, Loader2, Play, Eye } from 'lucide-react';
|
||||
import type { ReviewTask } from '../types';
|
||||
import { formatFileSize, formatTime } from '../api';
|
||||
|
||||
interface TaskTableProps {
|
||||
tasks: ReviewTask[];
|
||||
selectedIds: string[];
|
||||
onSelectChange: (ids: string[]) => void;
|
||||
onViewReport: (task: ReviewTask) => void;
|
||||
onRunTask: (task: ReviewTask) => void;
|
||||
}
|
||||
|
||||
export default function TaskTable({
|
||||
tasks,
|
||||
selectedIds,
|
||||
onSelectChange,
|
||||
onViewReport,
|
||||
onRunTask
|
||||
}: TaskTableProps) {
|
||||
const allSelected = tasks.length > 0 && selectedIds.length === tasks.length;
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
if (allSelected) {
|
||||
onSelectChange([]);
|
||||
} else {
|
||||
onSelectChange(tasks.map(t => t.id));
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSelect = (id: string) => {
|
||||
if (selectedIds.includes(id)) {
|
||||
onSelectChange(selectedIds.filter(i => i !== id));
|
||||
} else {
|
||||
onSelectChange([...selectedIds, id]);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取文件图标
|
||||
const getFileIcon = (fileName: string) => {
|
||||
if (fileName.endsWith('.pdf')) {
|
||||
return <FileText className="w-5 h-5" />;
|
||||
}
|
||||
return <FileType2 className="w-5 h-5" />;
|
||||
};
|
||||
|
||||
// 获取文件图标容器样式
|
||||
const getFileIconStyle = (fileName: string) => {
|
||||
if (fileName.endsWith('.pdf')) {
|
||||
return 'bg-red-50 text-red-600 border-red-100';
|
||||
}
|
||||
return 'bg-blue-50 text-blue-600 border-blue-100';
|
||||
};
|
||||
|
||||
// 渲染智能体标签
|
||||
const renderAgentTags = (task: ReviewTask) => {
|
||||
if (!task.selectedAgents || task.selectedAgents.length === 0) {
|
||||
return <span className="tag tag-gray">未运行</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-1.5">
|
||||
{task.selectedAgents.includes('editorial') && (
|
||||
<span className="tag tag-blue">规范性</span>
|
||||
)}
|
||||
{task.selectedAgents.includes('methodology') && (
|
||||
<span className="tag tag-purple">方法学</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染结果摘要
|
||||
const renderResultSummary = (task: ReviewTask) => {
|
||||
if (task.status === 'pending') {
|
||||
return <span className="text-xs text-slate-400 italic">等待发起...</span>;
|
||||
}
|
||||
|
||||
if (task.status === 'extracting' || task.status === 'reviewing') {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-xs text-indigo-600">
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
<span>{task.status === 'extracting' ? '提取文本中...' : '审查中...'}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (task.status === 'failed') {
|
||||
return <span className="text-xs text-red-500">失败</span>;
|
||||
}
|
||||
|
||||
if (task.status === 'completed') {
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{task.editorialScore !== undefined && (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<div className={`w-2 h-2 rounded-full ${task.editorialScore >= 80 ? 'bg-green-500' : task.editorialScore >= 60 ? 'bg-amber-500' : 'bg-red-500'}`} />
|
||||
<span className="text-slate-600">规范性:</span>
|
||||
<span className={`font-bold ${task.editorialScore >= 80 ? 'text-green-700' : task.editorialScore >= 60 ? 'text-amber-700' : 'text-red-700'}`}>
|
||||
{task.editorialScore}分
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{task.methodologyStatus && (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<div className={`w-2 h-2 rounded-full ${
|
||||
task.methodologyStatus === '通过' ? 'bg-green-500' :
|
||||
task.methodologyStatus === '存疑' ? 'bg-amber-500' : 'bg-red-500'
|
||||
}`} />
|
||||
<span className="text-slate-600">方法学:</span>
|
||||
<span className={`font-bold ${
|
||||
task.methodologyStatus === '通过' ? 'text-green-700' :
|
||||
task.methodologyStatus === '存疑' ? 'text-amber-700' : 'text-red-700'
|
||||
}`}>
|
||||
{task.methodologyStatus}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// 渲染操作按钮
|
||||
const renderActions = (task: ReviewTask) => {
|
||||
if (task.status === 'completed') {
|
||||
return (
|
||||
<button
|
||||
onClick={() => onViewReport(task)}
|
||||
className="text-indigo-600 font-bold hover:bg-indigo-50 px-3 py-1.5 rounded-md transition-colors text-xs flex items-center gap-1"
|
||||
>
|
||||
<Eye className="w-3 h-3" />
|
||||
查看
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (task.status === 'pending') {
|
||||
return (
|
||||
<button
|
||||
onClick={() => onRunTask(task)}
|
||||
className="border border-indigo-200 text-indigo-600 hover:bg-indigo-50 px-3 py-1.5 rounded-md transition-colors text-xs font-medium flex items-center gap-1"
|
||||
>
|
||||
<Play className="w-3 h-3" />
|
||||
开始
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (task.status === 'extracting' || task.status === 'reviewing') {
|
||||
return (
|
||||
<span className="text-xs text-slate-400 flex items-center gap-1">
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
处理中
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-xl shadow-sm p-12 text-center">
|
||||
<FileText className="w-12 h-12 text-slate-300 mx-auto mb-4" />
|
||||
<p className="text-slate-500">暂无稿件,请上传新稿件开始审查</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="bg-gray-50 border-b border-gray-200 text-gray-500 font-semibold uppercase tracking-wider text-xs">
|
||||
<tr>
|
||||
<th className="px-6 py-4 w-12">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allSelected}
|
||||
onChange={toggleSelectAll}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 cursor-pointer"
|
||||
/>
|
||||
</th>
|
||||
<th className="px-6 py-4 w-1/3">文件名称 / 信息</th>
|
||||
<th className="px-6 py-4">上传时间</th>
|
||||
<th className="px-6 py-4">审稿维度</th>
|
||||
<th className="px-6 py-4">结果摘要</th>
|
||||
<th className="px-6 py-4 text-right">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{tasks.map(task => (
|
||||
<tr
|
||||
key={task.id}
|
||||
className={`hover:bg-slate-50 group transition-colors ${selectedIds.includes(task.id) ? 'bg-blue-50' : ''}`}
|
||||
>
|
||||
<td className="px-6 py-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.includes(task.id)}
|
||||
onChange={() => toggleSelect(task.id)}
|
||||
className="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 cursor-pointer"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`p-2.5 rounded-lg border ${getFileIconStyle(task.fileName)}`}>
|
||||
{getFileIcon(task.fileName)}
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={`font-bold text-slate-800 text-base mb-0.5 ${task.status === 'completed' ? 'cursor-pointer hover:text-indigo-600' : ''}`}
|
||||
onClick={() => task.status === 'completed' && onViewReport(task)}
|
||||
>
|
||||
{task.fileName}
|
||||
</div>
|
||||
<div className="text-xs text-slate-400 flex items-center gap-2">
|
||||
<span className="bg-slate-100 px-1.5 rounded">{formatFileSize(task.fileSize)}</span>
|
||||
{task.wordCount && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span>{task.wordCount.toLocaleString()} 字</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-slate-500 font-mono text-xs">
|
||||
{formatTime(task.createdAt)}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{renderAgentTags(task)}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{renderResultSummary(task)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
{renderActions(task)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
14
frontend/src/pages/rvw/components/index.ts
Normal file
14
frontend/src/pages/rvw/components/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* RVW组件导出
|
||||
*/
|
||||
export { default as Sidebar } from './Sidebar';
|
||||
export { default as Header } from './Header';
|
||||
export { default as FilterChips } from './FilterChips';
|
||||
export { default as TaskTable } from './TaskTable';
|
||||
export { default as BatchToolbar } from './BatchToolbar';
|
||||
export { default as AgentModal } from './AgentModal';
|
||||
export { default as ScoreRing } from './ScoreRing';
|
||||
export { default as EditorialReport } from './EditorialReport';
|
||||
export { default as MethodologyReport } from './MethodologyReport';
|
||||
export { default as ReportDetail } from './ReportDetail';
|
||||
|
||||
7
frontend/src/pages/rvw/index.ts
Normal file
7
frontend/src/pages/rvw/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* RVW模块入口
|
||||
*/
|
||||
export { default as RvwDashboard } from './Dashboard';
|
||||
export * from './types';
|
||||
export * from './api';
|
||||
|
||||
233
frontend/src/pages/rvw/styles.css
Normal file
233
frontend/src/pages/rvw/styles.css
Normal file
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* RVW模块样式
|
||||
* 基于原型图 V7 的高保真还原
|
||||
*/
|
||||
|
||||
/* ==================== 状态标签 ==================== */
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.tag-blue {
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
border-color: #dbeafe;
|
||||
}
|
||||
|
||||
.tag-purple {
|
||||
background: #f5f3ff;
|
||||
color: #6d28d9;
|
||||
border-color: #ede9fe;
|
||||
}
|
||||
|
||||
.tag-green {
|
||||
background: #f0fdf4;
|
||||
color: #15803d;
|
||||
border-color: #dcfce7;
|
||||
}
|
||||
|
||||
.tag-amber {
|
||||
background: #fffbeb;
|
||||
color: #b45309;
|
||||
border-color: #fef3c7;
|
||||
}
|
||||
|
||||
.tag-red {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
.tag-gray {
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* ==================== 筛选 Chips ==================== */
|
||||
.filter-chip {
|
||||
padding: 4px 12px;
|
||||
border-radius: 9999px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
color: #64748b;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.filter-chip:hover {
|
||||
background-color: #f1f5f9;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.filter-chip.active {
|
||||
background-color: #eff6ff;
|
||||
color: #2563eb;
|
||||
border-color: #bfdbfe;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ==================== 侧边栏 Tooltip ==================== */
|
||||
.sidebar-tooltip {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin-left: 12px;
|
||||
background: #1e293b;
|
||||
color: white;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
z-index: 50;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s, visibility 0.2s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sidebar-btn:hover .sidebar-tooltip {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* ==================== 动画 ==================== */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.slide-up {
|
||||
animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 表格悬停效果 ==================== */
|
||||
.task-table tbody tr {
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.task-table tbody tr:hover {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.task-table tbody tr.selected {
|
||||
background-color: #eff6ff;
|
||||
}
|
||||
|
||||
/* ==================== 评分环 ==================== */
|
||||
.score-circle {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.score-circle.pass {
|
||||
border-color: #22c55e;
|
||||
background: #f0fdf4;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.score-circle.warn {
|
||||
border-color: #f59e0b;
|
||||
background: #fffbeb;
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.score-circle.fail {
|
||||
border-color: #ef4444;
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
/* ==================== 按钮样式 ==================== */
|
||||
.btn-primary {
|
||||
background-color: #4f46e5;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #4338ca;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: white;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d1d5db;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #f9fafb;
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
|
||||
/* ==================== 滚动条美化 ==================== */
|
||||
.overflow-auto::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.overflow-auto::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.overflow-auto::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.overflow-auto::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
/* ==================== 响应式调整 ==================== */
|
||||
@media (max-width: 1024px) {
|
||||
.sidebar-tooltip {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
92
frontend/src/pages/rvw/types.ts
Normal file
92
frontend/src/pages/rvw/types.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* RVW模块类型定义
|
||||
*/
|
||||
|
||||
// 任务状态
|
||||
export type TaskStatus =
|
||||
| 'pending' // 待处理
|
||||
| 'extracting' // 提取文本中
|
||||
| 'reviewing' // 审查中
|
||||
| 'completed' // 已完成
|
||||
| 'failed'; // 失败
|
||||
|
||||
// 智能体类型
|
||||
export type AgentType = 'editorial' | 'methodology';
|
||||
|
||||
// 审查任务
|
||||
export interface ReviewTask {
|
||||
id: string;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
status: TaskStatus;
|
||||
selectedAgents: AgentType[];
|
||||
wordCount?: number;
|
||||
overallScore?: number;
|
||||
editorialScore?: number;
|
||||
methodologyStatus?: string;
|
||||
errorMessage?: string;
|
||||
createdAt: string;
|
||||
completedAt?: string;
|
||||
durationSeconds?: number;
|
||||
}
|
||||
|
||||
// 规范性评估项
|
||||
export interface EditorialItem {
|
||||
criterion: string;
|
||||
status: 'pass' | 'warning' | 'fail';
|
||||
score: number;
|
||||
issues?: string[];
|
||||
suggestions?: string[];
|
||||
}
|
||||
|
||||
// 规范性评估结果
|
||||
export interface EditorialReviewResult {
|
||||
overall_score: number;
|
||||
summary: string;
|
||||
items: EditorialItem[];
|
||||
}
|
||||
|
||||
// 方法学问题
|
||||
export interface MethodologyIssue {
|
||||
type: string;
|
||||
severity: 'major' | 'minor';
|
||||
description: string;
|
||||
location: string;
|
||||
suggestion: string;
|
||||
}
|
||||
|
||||
// 方法学评估部分
|
||||
export interface MethodologyPart {
|
||||
part: string;
|
||||
score: number;
|
||||
issues: MethodologyIssue[];
|
||||
}
|
||||
|
||||
// 方法学评估结果
|
||||
export interface MethodologyReviewResult {
|
||||
overall_score: number;
|
||||
summary: string;
|
||||
parts: MethodologyPart[];
|
||||
}
|
||||
|
||||
// 完整审查报告
|
||||
export interface ReviewReport extends ReviewTask {
|
||||
editorialReview?: EditorialReviewResult;
|
||||
methodologyReview?: MethodologyReviewResult;
|
||||
modelUsed?: string;
|
||||
}
|
||||
|
||||
// API响应
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// 筛选条件
|
||||
export interface TaskFilters {
|
||||
status: 'all' | 'pending' | 'completed';
|
||||
timeRange: 'all' | 'today' | 'week';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user