refactor(asl): ASL frontend architecture refactoring with left navigation
- feat: Create ASLLayout component with 7-module left navigation - feat: Implement Title Screening Settings page with optimized PICOS layout - feat: Add placeholder pages for Workbench and Results - fix: Fix nested routing structure for React Router v6 - fix: Resolve Spin component warning in MainLayout - fix: Add QueryClientProvider to App.tsx - style: Optimize PICOS form layout (P+I left, C+O+S right) - style: Align Inclusion/Exclusion criteria side-by-side - docs: Add architecture refactoring and routing fix reports Ref: Week 2 Frontend Development Scope: ASL module MVP - Title Abstract Screening
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { ConfigProvider } from 'antd'
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { PermissionProvider } from './framework/permission'
|
||||
import { RouteGuard } from './framework/router'
|
||||
import MainLayout from './framework/layout/MainLayout'
|
||||
@@ -12,18 +13,34 @@ import { MODULES } from './framework/modules/moduleRegistry'
|
||||
*
|
||||
* @description
|
||||
* - ConfigProvider: Ant Design国际化配置
|
||||
* - QueryClientProvider: React Query状态管理(Week 2 新增)⭐
|
||||
* - PermissionProvider: 权限管理系统(Week 2 Day 7新增)
|
||||
* - RouteGuard: 路由守卫保护(Week 2 Day 7新增)⭐
|
||||
* - BrowserRouter: 前端路由
|
||||
*
|
||||
* @version Week 2 Day 7 - 任务17:完整版权限系统
|
||||
* @version Week 2 Day 1 - 添加React Query支持
|
||||
*/
|
||||
|
||||
// 创建React Query客户端
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5分钟
|
||||
gcTime: 1000 * 60 * 10, // 10分钟(原cacheTime)
|
||||
retry: 1, // 失败重试1次
|
||||
refetchOnWindowFocus: false, // 窗口聚焦时不自动重新获取
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ConfigProvider locale={zhCN}>
|
||||
{/* 权限提供者:提供全局权限状态 */}
|
||||
<PermissionProvider>
|
||||
<BrowserRouter>
|
||||
{/* React Query状态管理 */}
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{/* 权限提供者:提供全局权限状态 */}
|
||||
<PermissionProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<MainLayout />}>
|
||||
{/* 首页 */}
|
||||
@@ -52,6 +69,7 @@ function App() {
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</PermissionProvider>
|
||||
</QueryClientProvider>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ const MainLayout = () => {
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<Spin size="large" tip="加载中..." />
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -140,3 +140,5 @@ export const PermissionProvider = ({ children }: PermissionProviderProps) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -15,3 +15,5 @@ export { VERSION_LEVEL, checkVersionLevel } from './types'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -87,3 +87,5 @@ export const checkVersionLevel = (
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -44,3 +44,5 @@ export type { UserInfo, UserVersion, PermissionContextType } from './types'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -154,3 +154,5 @@ export default PermissionDenied
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -143,3 +143,5 @@ export default RouteGuard
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -13,3 +13,5 @@ export { default as PermissionDenied } from './PermissionDenied'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,3 +18,5 @@ export default AIAModule
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
267
frontend-v2/src/modules/asl/api/index.ts
Normal file
267
frontend-v2/src/modules/asl/api/index.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* ASL模块 - API客户端
|
||||
*
|
||||
* 负责所有与后端的API交互
|
||||
*/
|
||||
|
||||
import type {
|
||||
ScreeningProject,
|
||||
CreateProjectRequest,
|
||||
Literature,
|
||||
ImportLiteraturesRequest,
|
||||
ScreeningResult,
|
||||
ScreeningTask,
|
||||
ApiResponse,
|
||||
PaginatedResponse,
|
||||
ProjectStatistics
|
||||
} from '../types';
|
||||
|
||||
// API基础URL
|
||||
const API_BASE_URL = '/api/v1/asl';
|
||||
|
||||
// 通用请求函数
|
||||
async function request<T = any>(
|
||||
url: string,
|
||||
options?: RequestInit
|
||||
): Promise<T> {
|
||||
const response = await fetch(`${API_BASE_URL}${url}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({
|
||||
message: 'Network error'
|
||||
}));
|
||||
throw new Error(error.message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// ==================== 项目管理API ====================
|
||||
|
||||
/**
|
||||
* 创建筛选项目
|
||||
*/
|
||||
export async function createProject(
|
||||
data: CreateProjectRequest
|
||||
): Promise<ApiResponse<ScreeningProject>> {
|
||||
return request('/projects', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目列表
|
||||
*/
|
||||
export async function listProjects(): Promise<ApiResponse<ScreeningProject[]>> {
|
||||
return request('/projects');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目详情
|
||||
*/
|
||||
export async function getProject(
|
||||
projectId: string
|
||||
): Promise<ApiResponse<ScreeningProject>> {
|
||||
return request(`/projects/${projectId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新项目
|
||||
*/
|
||||
export async function updateProject(
|
||||
projectId: string,
|
||||
data: Partial<CreateProjectRequest>
|
||||
): Promise<ApiResponse<ScreeningProject>> {
|
||||
return request(`/projects/${projectId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除项目
|
||||
*/
|
||||
export async function deleteProject(
|
||||
projectId: string
|
||||
): Promise<ApiResponse<void>> {
|
||||
return request(`/projects/${projectId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 文献管理API ====================
|
||||
|
||||
/**
|
||||
* 批量导入文献(JSON格式)
|
||||
*/
|
||||
export async function importLiteratures(
|
||||
projectId: string,
|
||||
data: ImportLiteraturesRequest
|
||||
): Promise<ApiResponse<{
|
||||
imported: number;
|
||||
duplicates: number;
|
||||
failed: number;
|
||||
}>> {
|
||||
return request(`/projects/${projectId}/literatures/import-json`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文献列表
|
||||
*/
|
||||
export async function listLiteratures(
|
||||
projectId: string,
|
||||
params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
): Promise<ApiResponse<PaginatedResponse<Literature>>> {
|
||||
const queryString = new URLSearchParams(
|
||||
params as Record<string, string>
|
||||
).toString();
|
||||
return request(`/projects/${projectId}/literatures?${queryString}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文献
|
||||
*/
|
||||
export async function deleteLiterature(
|
||||
projectId: string,
|
||||
literatureId: string
|
||||
): Promise<ApiResponse<void>> {
|
||||
return request(`/projects/${projectId}/literatures/${literatureId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 筛选任务API ====================
|
||||
|
||||
/**
|
||||
* 启动筛选任务
|
||||
*/
|
||||
export async function startScreening(
|
||||
projectId: string
|
||||
): Promise<ApiResponse<{ taskId: string }>> {
|
||||
return request(`/projects/${projectId}/screening/start`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询任务进度
|
||||
*/
|
||||
export async function getTaskProgress(
|
||||
taskId: string
|
||||
): Promise<ApiResponse<ScreeningTask>> {
|
||||
return request(`/screening/tasks/${taskId}/progress`);
|
||||
}
|
||||
|
||||
// ==================== 筛选结果API ====================
|
||||
|
||||
/**
|
||||
* 获取筛选结果列表
|
||||
*/
|
||||
export async function getScreeningResults(
|
||||
projectId: string,
|
||||
params?: {
|
||||
conflictOnly?: boolean;
|
||||
finalDecision?: 'include' | 'exclude' | 'pending';
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
): Promise<ApiResponse<PaginatedResponse<ScreeningResult>>> {
|
||||
const queryString = new URLSearchParams(
|
||||
params as Record<string, string>
|
||||
).toString();
|
||||
return request(`/projects/${projectId}/screening/results?${queryString}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单个筛选结果
|
||||
*/
|
||||
export async function updateScreeningResult(
|
||||
resultId: string,
|
||||
data: {
|
||||
finalDecision?: 'include' | 'exclude' | 'pending';
|
||||
reviewComment?: string;
|
||||
}
|
||||
): Promise<ApiResponse<ScreeningResult>> {
|
||||
return request(`/screening/results/${resultId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新筛选结果
|
||||
*/
|
||||
export async function batchUpdateScreeningResults(data: {
|
||||
resultIds: string[];
|
||||
finalDecision: 'include' | 'exclude' | 'pending';
|
||||
decisionMethod?: string;
|
||||
reviewComment?: string;
|
||||
}): Promise<ApiResponse<{ updated: number }>> {
|
||||
return request('/screening/results/batch-update', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 导出API ====================
|
||||
|
||||
/**
|
||||
* 导出筛选结果为Excel
|
||||
*/
|
||||
export async function exportScreeningResults(
|
||||
projectId: string,
|
||||
params?: {
|
||||
filter?: 'all' | 'included' | 'excluded' | 'pending';
|
||||
}
|
||||
): Promise<Blob> {
|
||||
const queryString = new URLSearchParams(
|
||||
params as Record<string, string>
|
||||
).toString();
|
||||
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/projects/${projectId}/screening/results/export?${queryString}`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
// ==================== 统计API ====================
|
||||
|
||||
/**
|
||||
* 获取项目统计信息
|
||||
*/
|
||||
export async function getProjectStatistics(
|
||||
projectId: string
|
||||
): Promise<ApiResponse<ProjectStatistics>> {
|
||||
return request(`/projects/${projectId}/statistics`);
|
||||
}
|
||||
|
||||
// ==================== 健康检查API ====================
|
||||
|
||||
/**
|
||||
* 模块健康检查
|
||||
*/
|
||||
export async function healthCheck(): Promise<ApiResponse<{
|
||||
status: string;
|
||||
module: string;
|
||||
}>> {
|
||||
return request('/health');
|
||||
}
|
||||
|
||||
152
frontend-v2/src/modules/asl/components/ASLLayout.tsx
Normal file
152
frontend-v2/src/modules/asl/components/ASLLayout.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* ASL模块布局组件
|
||||
*
|
||||
* 左侧导航栏 + 右侧内容区
|
||||
* 参考原型:AI智能文献-标题摘要初筛原型.html
|
||||
*/
|
||||
|
||||
import { Layout, Menu } from 'antd';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
FileTextOutlined,
|
||||
SearchOutlined,
|
||||
FolderOpenOutlined,
|
||||
FilterOutlined,
|
||||
FileSearchOutlined,
|
||||
DatabaseOutlined,
|
||||
BarChartOutlined,
|
||||
SettingOutlined,
|
||||
CheckSquareOutlined,
|
||||
UnorderedListOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { MenuProps } from 'antd';
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
|
||||
type MenuItem = Required<MenuProps>['items'][number];
|
||||
|
||||
const ASLLayout = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// 菜单项配置
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
key: 'research-plan',
|
||||
icon: <FileTextOutlined />,
|
||||
label: '1. 研究方案生成',
|
||||
disabled: true,
|
||||
title: '敬请期待'
|
||||
},
|
||||
{
|
||||
key: 'literature-search',
|
||||
icon: <SearchOutlined />,
|
||||
label: '2. 智能文献检索',
|
||||
disabled: true,
|
||||
title: '敬请期待'
|
||||
},
|
||||
{
|
||||
key: 'literature-management',
|
||||
icon: <FolderOpenOutlined />,
|
||||
label: '3. 文献管理',
|
||||
disabled: true,
|
||||
title: '敬请期待'
|
||||
},
|
||||
{
|
||||
key: 'title-screening',
|
||||
icon: <FilterOutlined />,
|
||||
label: '4. 标题摘要初筛',
|
||||
children: [
|
||||
{
|
||||
key: '/literature/screening/title/settings',
|
||||
icon: <SettingOutlined />,
|
||||
label: '设置与启动',
|
||||
},
|
||||
{
|
||||
key: '/literature/screening/title/workbench',
|
||||
icon: <CheckSquareOutlined />,
|
||||
label: '审核工作台',
|
||||
},
|
||||
{
|
||||
key: '/literature/screening/title/results',
|
||||
icon: <UnorderedListOutlined />,
|
||||
label: '初筛结果',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'fulltext-screening',
|
||||
icon: <FileSearchOutlined />,
|
||||
label: '5. 全文复筛',
|
||||
disabled: true,
|
||||
title: '敬请期待'
|
||||
},
|
||||
{
|
||||
key: 'data-extraction',
|
||||
icon: <DatabaseOutlined />,
|
||||
label: '6. 全文解析与数据提取',
|
||||
disabled: true,
|
||||
title: '敬请期待'
|
||||
},
|
||||
{
|
||||
key: 'data-analysis',
|
||||
icon: <BarChartOutlined />,
|
||||
label: '7. 数据综合分析与报告',
|
||||
disabled: true,
|
||||
title: '敬请期待'
|
||||
},
|
||||
];
|
||||
|
||||
// 处理菜单点击
|
||||
const handleMenuClick: MenuProps['onClick'] = ({ key }) => {
|
||||
if (key.startsWith('/')) {
|
||||
navigate(key);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前选中的菜单项和展开的子菜单
|
||||
const currentPath = location.pathname;
|
||||
const selectedKeys = [currentPath];
|
||||
const openKeys = currentPath.includes('screening/title') ? ['title-screening'] : [];
|
||||
|
||||
return (
|
||||
<Layout className="h-screen">
|
||||
{/* 左侧导航栏 */}
|
||||
<Sider
|
||||
width={250}
|
||||
className="bg-gray-50 border-r border-gray-200"
|
||||
theme="light"
|
||||
>
|
||||
<div className="p-4 border-b border-gray-200">
|
||||
<h2 className="text-lg font-bold text-gray-800">
|
||||
<FilterOutlined className="mr-2" />
|
||||
AI智能文献
|
||||
</h2>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
标题摘要初筛 MVP
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={selectedKeys}
|
||||
defaultOpenKeys={openKeys}
|
||||
items={menuItems}
|
||||
onClick={handleMenuClick}
|
||||
className="border-none"
|
||||
style={{ height: 'calc(100vh - 80px)', overflowY: 'auto' }}
|
||||
/>
|
||||
</Sider>
|
||||
|
||||
{/* 右侧内容区 */}
|
||||
<Layout>
|
||||
<Content className="bg-white overflow-auto">
|
||||
<Outlet />
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ASLLayout;
|
||||
|
||||
@@ -1,16 +1,45 @@
|
||||
import Placeholder from '@/shared/components/Placeholder'
|
||||
/**
|
||||
* ASL模块入口
|
||||
* AI智能文献筛选模块
|
||||
*/
|
||||
|
||||
import { Suspense, lazy } from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { Spin } from 'antd';
|
||||
|
||||
// 懒加载组件
|
||||
const ASLLayout = lazy(() => import('./components/ASLLayout'));
|
||||
const TitleScreeningSettings = lazy(() => import('./pages/TitleScreeningSettings'));
|
||||
const TitleScreeningWorkbench = lazy(() => import('./pages/ScreeningWorkbench'));
|
||||
const TitleScreeningResults = lazy(() => import('./pages/ScreeningResults'));
|
||||
|
||||
const ASLModule = () => {
|
||||
return (
|
||||
<Placeholder
|
||||
title="AI智能文献模块"
|
||||
description="Week 3 开始开发,支持4个LLM的智能文献筛选和分析"
|
||||
moduleName="ASL - AI Smart Literature"
|
||||
/>
|
||||
)
|
||||
}
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="" element={<ASLLayout />}>
|
||||
<Route index element={<Navigate to="screening/title/settings" replace />} />
|
||||
<Route path="screening/title">
|
||||
<Route index element={<Navigate to="settings" replace />} />
|
||||
<Route path="settings" element={<TitleScreeningSettings />} />
|
||||
<Route path="workbench" element={<TitleScreeningWorkbench />} />
|
||||
<Route path="results" element={<TitleScreeningResults />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default ASLModule;
|
||||
|
||||
|
||||
export default ASLModule
|
||||
|
||||
|
||||
|
||||
|
||||
44
frontend-v2/src/modules/asl/pages/ScreeningResults.tsx
Normal file
44
frontend-v2/src/modules/asl/pages/ScreeningResults.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 标题摘要初筛 - 初筛结果页面
|
||||
* TODO: Week 2 Day 5 开发
|
||||
*
|
||||
* 功能:
|
||||
* - 统计卡片(总数/纳入/排除)
|
||||
* - PRISMA排除原因统计
|
||||
* - Tab切换(纳入/排除)
|
||||
* - 结果表格
|
||||
* - 批量操作
|
||||
* - 导出Excel
|
||||
*/
|
||||
|
||||
import { Card, Empty, Alert } from 'antd';
|
||||
|
||||
const TitleScreeningResults = () => {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold mb-2">标题摘要初筛 - 结果</h1>
|
||||
<p className="text-gray-500">
|
||||
筛选结果统计、PRISMA流程图、批量操作和导出
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Alert
|
||||
message="功能开发中"
|
||||
description="Week 2 Day 5 将实现统计卡片、结果表格、批量操作、Excel导出等功能"
|
||||
type="info"
|
||||
showIcon
|
||||
className="mb-4"
|
||||
/>
|
||||
<Empty
|
||||
description="初筛结果页(开发中)"
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TitleScreeningResults;
|
||||
|
||||
42
frontend-v2/src/modules/asl/pages/ScreeningWorkbench.tsx
Normal file
42
frontend-v2/src/modules/asl/pages/ScreeningWorkbench.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 标题摘要初筛 - 审核工作台页面
|
||||
* TODO: Week 2 Day 3-4 开发
|
||||
*
|
||||
* 功能:
|
||||
* - 显示当前PICOS标准(折叠面板)
|
||||
* - 双行表格(严格按照原型)
|
||||
* - 点击PICO维度 → 弹出双视图Modal
|
||||
* - 修改最终决策
|
||||
*/
|
||||
|
||||
import { Card, Empty, Alert } from 'antd';
|
||||
|
||||
const TitleScreeningWorkbench = () => {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold mb-2">标题摘要初筛 - 审核工作台</h1>
|
||||
<p className="text-gray-500">
|
||||
双行表格展示筛选结果,支持人工复核和决策修改
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Alert
|
||||
message="功能开发中"
|
||||
description="Week 2 Day 3-4 将实现双行表格、双视图Modal、人工复核等功能"
|
||||
type="info"
|
||||
showIcon
|
||||
className="mb-4"
|
||||
/>
|
||||
<Empty
|
||||
description="审核工作台(开发中)"
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TitleScreeningWorkbench;
|
||||
|
||||
347
frontend-v2/src/modules/asl/pages/TitleScreeningSettings.tsx
Normal file
347
frontend-v2/src/modules/asl/pages/TitleScreeningSettings.tsx
Normal file
@@ -0,0 +1,347 @@
|
||||
/**
|
||||
* 标题摘要初筛 - 设置与启动页面
|
||||
*
|
||||
* 功能:
|
||||
* 1. Excel文献导入(上传 + 模板下载)
|
||||
* 2. PICOS标准配置
|
||||
* 3. 纳入/排除标准配置
|
||||
* 4. 筛选风格选择
|
||||
* 5. 启动AI筛选
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
Upload,
|
||||
Radio,
|
||||
Space,
|
||||
Tooltip,
|
||||
message,
|
||||
Alert,
|
||||
Divider,
|
||||
Row,
|
||||
Col,
|
||||
} from 'antd';
|
||||
import {
|
||||
InboxOutlined,
|
||||
QuestionCircleOutlined,
|
||||
DownloadOutlined,
|
||||
PlayCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Dragger } = Upload;
|
||||
|
||||
const TitleScreeningSettings = () => {
|
||||
const navigate = useNavigate();
|
||||
const [form] = Form.useForm();
|
||||
const [fileList, setFileList] = useState<any[]>([]);
|
||||
const [literatureCount, setLiteratureCount] = useState(0);
|
||||
const [canStart, setCanStart] = useState(false);
|
||||
|
||||
// 处理Excel上传
|
||||
const handleFileUpload = async (file: File) => {
|
||||
try {
|
||||
// TODO: Week 2 Day 2 实现Excel解析
|
||||
// 这里只是占位逻辑
|
||||
setFileList([file]);
|
||||
setLiteratureCount(100); // 模拟导入100篇文献
|
||||
setCanStart(true);
|
||||
message.success(`成功导入 100 篇文献`);
|
||||
return false; // 阻止自动上传
|
||||
} catch (error) {
|
||||
message.error('文献导入失败');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 下载Excel模板
|
||||
const handleDownloadTemplate = () => {
|
||||
// TODO: Week 2 Day 2 实现模板下载
|
||||
message.info('Excel模板下载功能开发中...');
|
||||
};
|
||||
|
||||
// 启动筛选
|
||||
const handleStartScreening = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
// TODO: Week 2 调用后端API启动筛选
|
||||
console.log('启动筛选:', values);
|
||||
|
||||
message.success('AI筛选已启动,正在处理中...');
|
||||
|
||||
// 跳转到审核工作台
|
||||
navigate('/literature/screening/title/workbench');
|
||||
} catch (error) {
|
||||
message.error('请完整填写筛选标准');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
{/* 页面标题 */}
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold mb-2">标题摘要初筛 - 设置与启动</h1>
|
||||
<p className="text-gray-500">
|
||||
配置PICOS标准、纳入/排除标准,然后导入文献开始AI筛选
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
screeningStyle: 'standard',
|
||||
}}
|
||||
>
|
||||
{/* 步骤1: 配置筛选标准 */}
|
||||
<Card title="步骤1: 配置筛选标准" className="mb-6">
|
||||
{/* PICOS标准 */}
|
||||
<Alert
|
||||
message="PICOS标准"
|
||||
description="系统评价研究问题的标准化框架,请详细填写每个维度"
|
||||
type="info"
|
||||
showIcon
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
<Row gutter={16}>
|
||||
{/* 左侧:P + I */}
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-base font-semibold">
|
||||
P - 人群 (Population)
|
||||
<Tooltip title="研究对象的特征,如年龄、性别、疾病类型等。可以包含主要人群和亚组人群。">
|
||||
<QuestionCircleOutlined className="ml-2 text-gray-400" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name={['picoCriteria', 'P']}
|
||||
rules={[{ required: true, message: '请输入人群标准' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={10}
|
||||
placeholder="例如: Patients with non-cardioembolic ischemic stroke (NCIS) 非心源性缺血性卒中、亚洲人群 亚组人群: 1. NIHSS评分亚组卒中人群(mild/moderate stroke) 2. 不同TOAST分型(different TOAST subtypes,excluding cardioembolic stroke) 3. 高危TIA人群(high-risk TIA population) ..."
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-base font-semibold">
|
||||
I - 干预 (Intervention)
|
||||
<Tooltip title="研究的干预措施或暴露因素">
|
||||
<QuestionCircleOutlined className="ml-2 text-gray-400" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name={['picoCriteria', 'I']}
|
||||
rules={[{ required: true, message: '请输入干预措施' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={5}
|
||||
placeholder="例如:抗血小板治疗药物(阿司匹林,氯吡格雷等)、抗凝药物(华法林等)、溶栓药物(阿替普酶等)"
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
{/* 右侧:C + O + S */}
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-base font-semibold">
|
||||
C - 对照 (Comparison)
|
||||
<Tooltip title="对照组的干预措施">
|
||||
<QuestionCircleOutlined className="ml-2 text-gray-400" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name={['picoCriteria', 'C']}
|
||||
rules={[{ required: true, message: '请输入对照措施' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={5}
|
||||
placeholder="例如:安慰剂、常规治疗、其他药物对照等"
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-base font-semibold">
|
||||
O - 结局 (Outcome)
|
||||
<Tooltip title="研究的主要结局指标(可选)">
|
||||
<QuestionCircleOutlined className="ml-2 text-gray-400" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name={['picoCriteria', 'O']}
|
||||
>
|
||||
<TextArea
|
||||
rows={5}
|
||||
placeholder="例如:卒中进展、卒中复发、死亡、NIHSS评分变化、疗效、安全性等"
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-base font-semibold">
|
||||
S - 研究设计 (Study Design)
|
||||
<Tooltip title="纳入的研究类型">
|
||||
<QuestionCircleOutlined className="ml-2 text-gray-400" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name={['picoCriteria', 'S']}
|
||||
rules={[{ required: true, message: '请输入研究设计' }]}
|
||||
>
|
||||
<Input
|
||||
placeholder="例如:系统评价(SR)、随机对照试验(RCT)、真实世界研究(RWE)、观察性研究(OBS)"
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 纳入标准 & 排除标准 - 并排显示 */}
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-base font-semibold">
|
||||
纳入标准
|
||||
<Tooltip title="文献必须满足的条件才能被纳入">
|
||||
<QuestionCircleOutlined className="ml-2 text-gray-400" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="inclusionCriteria"
|
||||
rules={[{ required: true, message: '请输入纳入标准' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={10}
|
||||
placeholder="详细的纳入标准,例如: 1. 非心源性缺血性卒中、亚洲患者 2. 包含二级预防相关研究 3. 涉及抗血小板或抗凝药物 4. 研究类型:SR、RCT、RWE、OBS 5. 近五年(2020年之后)的文献 ..."
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label={
|
||||
<span className="text-base font-semibold">
|
||||
排除标准
|
||||
<Tooltip title="满足这些条件的文献将被排除">
|
||||
<QuestionCircleOutlined className="ml-2 text-gray-400" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
name="exclusionCriteria"
|
||||
rules={[{ required: true, message: '请输入排除标准' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={10}
|
||||
placeholder="详细的排除标准,例如: 1. 心源性卒中患者、非亚洲人群 2. 急性期治疗研究(无二级预防关键词) 3. 病例报告、会议摘要 4. 非中英文文献 5. 混合人群研究 ..."
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 筛选风格 */}
|
||||
<Form.Item
|
||||
label={<span className="text-base font-semibold">筛选风格</span>}
|
||||
name="screeningStyle"
|
||||
extra="提示:初筛推荐宽松模式,精筛推荐严格模式"
|
||||
>
|
||||
<Radio.Group size="large">
|
||||
<Space direction="vertical" className="w-full">
|
||||
<Radio value="lenient">
|
||||
🔓 宽松模式 - 初筛推荐,宁可多纳入不错过
|
||||
</Radio>
|
||||
<Radio value="standard">
|
||||
⚖️ 标准模式(推荐)- 平衡准确率和召回率
|
||||
</Radio>
|
||||
<Radio value="strict">
|
||||
🔒 严格模式 - 精筛推荐,保证质量
|
||||
</Radio>
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
{/* 步骤2: 导入文献 */}
|
||||
<Card title="步骤2: 导入文献" className="mb-6">
|
||||
<div className="text-center">
|
||||
<Dragger
|
||||
accept=".xlsx,.xls"
|
||||
maxCount={1}
|
||||
fileList={fileList}
|
||||
beforeUpload={handleFileUpload}
|
||||
onRemove={() => {
|
||||
setFileList([]);
|
||||
setLiteratureCount(0);
|
||||
setCanStart(false);
|
||||
}}
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined style={{ fontSize: 48, color: '#1890ff' }} />
|
||||
</p>
|
||||
<p className="ant-upload-text">点击或拖拽Excel文件到此区域</p>
|
||||
<p className="ant-upload-hint">
|
||||
支持 .xlsx 和 .xls 格式,必须包含 Title 和 Abstract 列
|
||||
</p>
|
||||
</Dragger>
|
||||
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleDownloadTemplate}
|
||||
>
|
||||
下载Excel模板
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{literatureCount > 0 && (
|
||||
<Alert
|
||||
message={`已导入 ${literatureCount} 篇文献`}
|
||||
type="success"
|
||||
showIcon
|
||||
className="mt-4"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Form>
|
||||
|
||||
{/* 启动按钮 */}
|
||||
<div className="text-center">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={handleStartScreening}
|
||||
disabled={!canStart}
|
||||
className="px-12"
|
||||
>
|
||||
{canStart ? '开始AI筛选' : '请先导入文献'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TitleScreeningSettings;
|
||||
|
||||
223
frontend-v2/src/modules/asl/types/index.ts
Normal file
223
frontend-v2/src/modules/asl/types/index.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* ASL模块 - TypeScript类型定义
|
||||
*
|
||||
* 包含:
|
||||
* - 项目管理类型
|
||||
* - 文献管理类型
|
||||
* - 筛选结果类型
|
||||
*/
|
||||
|
||||
// ==================== 项目管理类型 ====================
|
||||
|
||||
/**
|
||||
* PICOS标准
|
||||
*/
|
||||
export interface PICOSCriteria {
|
||||
P: string; // 人群 Population
|
||||
I: string; // 干预 Intervention
|
||||
C: string; // 对照 Comparison
|
||||
O?: string; // 结局 Outcome(可选)
|
||||
S: string; // 研究设计 Study Design
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选配置
|
||||
*/
|
||||
export interface ScreeningConfig {
|
||||
style: 'lenient' | 'standard' | 'strict'; // 筛选风格
|
||||
models: string[]; // 使用的模型列表
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选项目
|
||||
*/
|
||||
export interface ScreeningProject {
|
||||
id: string;
|
||||
projectName: string;
|
||||
picoCriteria: PICOSCriteria;
|
||||
inclusionCriteria: string;
|
||||
exclusionCriteria: string;
|
||||
screeningConfig: ScreeningConfig;
|
||||
status: 'draft' | 'screening' | 'completed';
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
userId: string;
|
||||
|
||||
// 统计信息(可选)
|
||||
stats?: {
|
||||
totalLiteratures: number;
|
||||
screenedCount: number;
|
||||
includedCount: number;
|
||||
excludedCount: number;
|
||||
pendingCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建项目请求
|
||||
*/
|
||||
export interface CreateProjectRequest {
|
||||
projectName: string;
|
||||
picoCriteria: PICOSCriteria;
|
||||
inclusionCriteria: string;
|
||||
exclusionCriteria: string;
|
||||
screeningConfig: ScreeningConfig;
|
||||
}
|
||||
|
||||
// ==================== 文献管理类型 ====================
|
||||
|
||||
/**
|
||||
* 文献条目
|
||||
*/
|
||||
export interface Literature {
|
||||
id: string;
|
||||
projectId: string;
|
||||
title: string;
|
||||
abstract: string;
|
||||
pmid?: string;
|
||||
authors?: string;
|
||||
journal?: string;
|
||||
publicationYear?: number;
|
||||
doi?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入文献请求
|
||||
*/
|
||||
export interface ImportLiteraturesRequest {
|
||||
literatures: Omit<Literature, 'id' | 'projectId' | 'createdAt'>[];
|
||||
}
|
||||
|
||||
// ==================== 筛选结果类型 ====================
|
||||
|
||||
/**
|
||||
* PICO判断
|
||||
*/
|
||||
export interface PICOJudgment {
|
||||
P: 'match' | 'mismatch' | 'unclear';
|
||||
I: 'match' | 'mismatch' | 'unclear';
|
||||
C: 'match' | 'mismatch' | 'unclear';
|
||||
S: 'match' | 'mismatch' | 'unclear';
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型判断结果
|
||||
*/
|
||||
export interface ModelResult {
|
||||
modelName: string; // 'DeepSeek-V3' | 'Qwen-Max'
|
||||
conclusion: 'include' | 'exclude';
|
||||
confidence: number; // 0-1
|
||||
reason: string; // 完整的判断理由
|
||||
judgment: PICOJudgment;
|
||||
evidence?: { // 提取的证据短语
|
||||
P?: string;
|
||||
I?: string;
|
||||
C?: string;
|
||||
S?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选结果
|
||||
*/
|
||||
export interface ScreeningResult {
|
||||
id: string;
|
||||
projectId: string;
|
||||
literatureId: string;
|
||||
|
||||
// 文献信息
|
||||
literature: Literature;
|
||||
|
||||
// 双模型结果
|
||||
model1Result: ModelResult;
|
||||
model2Result: ModelResult;
|
||||
|
||||
// 最终决策
|
||||
finalDecision: 'include' | 'exclude' | 'pending';
|
||||
decisionMethod: 'auto_agree' | 'auto_strict' | 'manual' | 'manual_batch';
|
||||
|
||||
// 冲突信息
|
||||
hasConflict: boolean;
|
||||
conflictFields: string[]; // ['conclusion', 'P', 'I', 'C', 'S']
|
||||
|
||||
// 人工复核
|
||||
reviewedAt?: string;
|
||||
reviewedBy?: string;
|
||||
reviewComment?: string;
|
||||
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选任务
|
||||
*/
|
||||
export interface ScreeningTask {
|
||||
id: string;
|
||||
projectId: string;
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed';
|
||||
totalItems: number;
|
||||
processedItems: number;
|
||||
successItems: number;
|
||||
failedItems: number;
|
||||
progress: number; // 0-100
|
||||
startedAt?: string;
|
||||
completedAt?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
// ==================== API响应类型 ====================
|
||||
|
||||
/**
|
||||
* 通用API响应
|
||||
*/
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: any;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应
|
||||
*/
|
||||
export interface PaginatedResponse<T = any> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
// ==================== 导出统计类型 ====================
|
||||
|
||||
/**
|
||||
* 项目统计
|
||||
*/
|
||||
export interface ProjectStatistics {
|
||||
totalLiteratures: number;
|
||||
screenedCount: number;
|
||||
includedCount: number;
|
||||
excludedCount: number;
|
||||
pendingCount: number;
|
||||
conflictCount: number;
|
||||
reviewedCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 排除原因统计
|
||||
*/
|
||||
export interface ExclusionReasons {
|
||||
[key: string]: number;
|
||||
nonRCT: number;
|
||||
populationMismatch: number;
|
||||
interventionMismatch: number;
|
||||
comparisonMismatch: number;
|
||||
other: number;
|
||||
}
|
||||
|
||||
@@ -18,3 +18,5 @@ export default DCModule
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,3 +18,5 @@ export default PKBModule
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,3 +22,5 @@ export default SSAModule
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -22,3 +22,5 @@ export default STModule
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -48,3 +48,5 @@ export default Placeholder
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user