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

@@ -0,0 +1,78 @@
/**
* 筛选结果列表Hook
* 用于获取和管理筛选结果列表(支持分页和筛选)
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { message } from 'antd';
import { aslApi } from '../api';
interface UseScreeningResultsOptions {
projectId: string;
page?: number;
pageSize?: number;
filter?: 'all' | 'conflict' | 'included' | 'excluded' | 'reviewed';
enabled?: boolean;
}
/**
* 使用筛选结果Hook
*/
export function useScreeningResults({
projectId,
page = 1,
pageSize = 50,
filter = 'all',
enabled = true,
}: UseScreeningResultsOptions) {
const queryClient = useQueryClient();
// 查询筛选结果列表
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['screening-results', projectId, page, pageSize, filter],
queryFn: () => aslApi.getScreeningResultsList(projectId, { page, pageSize, filter }),
enabled: enabled && !!projectId,
staleTime: 1000 * 30, // 30秒内认为数据是新鲜的
keepPreviousData: true, // 切换页面时保留上一页数据,避免闪烁
});
const results = data?.data?.items || [];
const total = data?.data?.total || 0;
const totalPages = data?.data?.totalPages || 0;
// 人工复核Mutation
const reviewMutation = useMutation({
mutationFn: ({ resultId, decision, note }: {
resultId: string;
decision: 'include' | 'exclude';
note?: string;
}) => aslApi.reviewScreeningResult(resultId, { decision, note }),
onSuccess: () => {
// 刷新列表
queryClient.invalidateQueries({ queryKey: ['screening-results', projectId] });
message.success('复核提交成功');
},
onError: (error: any) => {
message.error(`复核失败: ${error.message}`);
},
});
return {
results,
total,
totalPages,
page,
pageSize,
isLoading,
error,
refetch,
// 人工复核方法
review: reviewMutation.mutate,
isReviewing: reviewMutation.isPending,
};
}