feat(asl): Complete Week 4 - Results display and Excel export with hybrid solution
Features: - Backend statistics API (cloud-native Prisma aggregation) - Results page with hybrid solution (AI consensus + human final decision) - Excel export (frontend generation, zero disk write, cloud-native) - PRISMA-style exclusion reason analysis with bar chart - Batch selection and export (3 export methods) - Fixed logic contradiction (inclusion does not show exclusion reason) - Optimized table width (870px, no horizontal scroll) Components: - Backend: screeningController.ts - add getProjectStatistics API - Frontend: ScreeningResults.tsx - complete results page (hybrid solution) - Frontend: excelExport.ts - Excel export utility (40 columns full info) - Frontend: ScreeningWorkbench.tsx - add navigation button - Utils: get-test-projects.mjs - quick test tool Architecture: - Cloud-native: backend aggregation reduces network transfer - Cloud-native: frontend Excel generation (zero file persistence) - Reuse platform: global prisma instance, logger - Performance: statistics API < 500ms, Excel export < 3s (1000 records) Documentation: - Update module status guide (add Week 4 features) - Update task breakdown (mark Week 4 completed) - Update API design spec (add statistics API) - Update database design (add field usage notes) - Create Week 4 development plan - Create Week 4 completion report - Create technical debt list Test: - End-to-end flow test passed - All features verified - Performance test passed - Cloud-native compliance verified Ref: Week 4 Development Plan Scope: ASL Module MVP - Title Abstract Screening Results Cloud-Native: Backend aggregation + Frontend Excel generation
This commit is contained in:
@@ -8,7 +8,6 @@ import type {
|
||||
ScreeningProject,
|
||||
CreateProjectRequest,
|
||||
Literature,
|
||||
ImportLiteraturesRequest,
|
||||
ScreeningResult,
|
||||
ScreeningTask,
|
||||
ApiResponse,
|
||||
@@ -33,10 +32,20 @@ async function request<T = any>(
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({
|
||||
message: 'Network error'
|
||||
}));
|
||||
throw new Error(error.message || `HTTP ${response.status}`);
|
||||
// 尝试解析错误响应
|
||||
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMessage = errorData.error || errorData.message || errorMessage;
|
||||
} catch (e) {
|
||||
// 如果响应体不是JSON,使用状态文本
|
||||
const text = await response.text().catch(() => '');
|
||||
if (text) {
|
||||
errorMessage = text;
|
||||
}
|
||||
}
|
||||
console.error('❌ API请求失败:', { url: `${API_BASE_URL}${url}`, status: response.status, error: errorMessage });
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
@@ -251,6 +260,69 @@ export async function getProjectStatistics(
|
||||
return request(`/projects/${projectId}/statistics`);
|
||||
}
|
||||
|
||||
// ==================== Day 3 新增API ====================
|
||||
|
||||
/**
|
||||
* 获取筛选任务进度(新)
|
||||
* GET /projects/:projectId/screening-task
|
||||
*/
|
||||
export async function getScreeningTask(
|
||||
projectId: string
|
||||
): Promise<ApiResponse<ScreeningTask>> {
|
||||
return request(`/projects/${projectId}/screening-task`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取筛选结果列表(新,支持分页和筛选)
|
||||
* GET /projects/:projectId/screening-results
|
||||
*/
|
||||
export async function getScreeningResultsList(
|
||||
projectId: string,
|
||||
params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
filter?: 'all' | 'conflict' | 'included' | 'excluded' | 'pending' | 'reviewed';
|
||||
}
|
||||
): Promise<ApiResponse<{
|
||||
items: ScreeningResult[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
}>> {
|
||||
const queryString = new URLSearchParams(
|
||||
params as Record<string, string>
|
||||
).toString();
|
||||
return request(`/projects/${projectId}/screening-results?${queryString}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个筛选结果详情(新)
|
||||
* GET /screening-results/:resultId
|
||||
*/
|
||||
export async function getScreeningResultDetail(
|
||||
resultId: string
|
||||
): Promise<ApiResponse<ScreeningResult>> {
|
||||
return request(`/screening-results/${resultId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交人工复核(新)
|
||||
* POST /screening-results/:resultId/review
|
||||
*/
|
||||
export async function reviewScreeningResult(
|
||||
resultId: string,
|
||||
data: {
|
||||
decision: 'include' | 'exclude';
|
||||
note?: string;
|
||||
}
|
||||
): Promise<ApiResponse<ScreeningResult>> {
|
||||
return request(`/screening-results/${resultId}/review`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 健康检查API ====================
|
||||
|
||||
/**
|
||||
@@ -284,11 +356,15 @@ export const aslApi = {
|
||||
// 筛选任务
|
||||
startScreening,
|
||||
getTaskProgress,
|
||||
getScreeningTask, // Day 3 新增
|
||||
|
||||
// 筛选结果
|
||||
getScreeningResults,
|
||||
getScreeningResultsList, // Day 3 新增(分页版本)
|
||||
getScreeningResultDetail, // Day 3 新增
|
||||
updateScreeningResult,
|
||||
batchUpdateScreeningResults,
|
||||
reviewScreeningResult, // Day 3 新增(人工复核)
|
||||
|
||||
// 导出
|
||||
exportScreeningResults,
|
||||
|
||||
Reference in New Issue
Block a user