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:
2025-11-21 20:12:38 +08:00
parent 2e8699c217
commit 8eef9e0544
207 changed files with 11142 additions and 531 deletions

View File

@@ -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,