diff --git a/backend/app/api/tasks.py b/backend/app/api/tasks.py index d995bc0..070353f 100644 --- a/backend/app/api/tasks.py +++ b/backend/app/api/tasks.py @@ -21,19 +21,19 @@ from app.schemas.user import UserResponse router = APIRouter(prefix="/api/tasks", tags=["对账任务"]) -@router.get("/", response_model=List[Dict[str, Any]]) +@router.get("/", response_model=Dict[str, Any]) async def list_tasks( period: Optional[str] = Query(None, description="筛选月份,格式: 2026-04"), status: Optional[str] = Query(None, description="筛选状态"), - skip: int = Query(0, ge=0, description="跳过记录数"), - limit: int = Query(50, ge=1, le=100, description="返回记录数"), + page: int = Query(1, ge=1, description="页码"), + page_size: int = Query(10, ge=1, le=100, description="每页数量"), db: AsyncSession = Depends(get_db), company_id: Optional[int] = Depends(get_current_company_id), -) -> List[Dict[str, Any]]: +) -> Dict[str, Any]: """ 获取对账任务列表 - 支持按月份和状态筛选 + 支持按月份和状态筛选,返回分页结构 """ # 构建查询 query = select(ReconciliationTask).where( @@ -46,27 +46,45 @@ async def list_tasks( if status: query = query.where(ReconciliationTask.status == status) - # 按创建时间倒序 - query = query.order_by(ReconciliationTask.created_at.desc()) + # 计算总数 + count_query = select(func.count()).select_from(ReconciliationTask).where( + ReconciliationTask.company_id == company_id + ) + if period: + count_query = count_query.where(ReconciliationTask.period == period) + if status: + count_query = count_query.where(ReconciliationTask.status == status) + count_result = await db.execute(count_query) + total = count_result.scalar() or 0 - # 分页 - query = query.offset(skip).limit(limit) + # 按创建时间倒序 + 分页 + skip = (page - 1) * page_size + query = query.order_by(ReconciliationTask.created_at.desc()) + query = query.offset(skip).limit(page_size) result = await db.execute(query) tasks = result.scalars().all() - return [ - { - "id": task.id, - "period": task.period, - "status": task.status, - "total_employees": task.total_employees, - "matched_count": task.matched_count, - "exception_count": task.exception_count, - "created_at": task.created_at.isoformat() if task.created_at else None, - } - for task in tasks - ] + return { + "items": [ + { + "id": task.id, + "name": f"{task.period} 对账任务", + "period": task.period, + "status": task.status, + "total_employees": task.total_employees, + "matched_count": task.matched_count, + "exception_count": task.exception_count, + "created_at": task.created_at.isoformat() if task.created_at else None, + "completed_at": task.updated_at.isoformat() if task.status == "COMPLETED" and task.updated_at else None, + } + for task in tasks + ], + "total": total, + "page": page, + "page_size": page_size, + "total_pages": (total + page_size - 1) // page_size if page_size > 0 else 0, + } @router.get("/stats", response_model=Dict[str, Any]) @@ -169,6 +187,7 @@ async def get_task( return { "id": task.id, + "name": f"{task.period} 对账任务", "period": task.period, "status": task.status, "total_employees": task.total_employees, @@ -177,5 +196,6 @@ async def get_task( "file_ids": task.file_ids, "reconciliation_result": task.reconciliation_result, "created_at": task.created_at.isoformat() if task.created_at else None, + "completed_at": task.updated_at.isoformat() if task.status == "COMPLETED" and task.updated_at else None, "error_message": task.error_message, } diff --git a/frontend/app/(dashboard)/dashboard/page.tsx b/frontend/app/(dashboard)/dashboard/page.tsx index aa0b176..49bfe9b 100644 --- a/frontend/app/(dashboard)/dashboard/page.tsx +++ b/frontend/app/(dashboard)/dashboard/page.tsx @@ -79,11 +79,11 @@ export default function DashboardPage() { // 并行获取统计数据和任务列表 const [statsRes, tasksRes] = await Promise.all([ api.get(ENDPOINTS.TASK.STATS), - api.get(ENDPOINTS.TASK.LIST, { params: { limit: 5 } }), + api.get<{ items: Task[] }>(ENDPOINTS.TASK.LIST, { params: { page: 1, page_size: 5 } }), ]); setStats(statsRes.data); - setRecentTasks(tasksRes.data); + setRecentTasks(tasksRes.data.items || []); } catch (error) { console.error("加载数据失败:", error); } finally { @@ -101,7 +101,7 @@ export default function DashboardPage() { }; return ( -
+
{/* Header */} ) : ( - exceptions.map((exception, index) => ( - - - - - {statusConfig[exception.status]?.label || exception.status} + exceptions.map((exception) => ( + + + + {statusConfig[exception.status]?.label || exception.status} + + + +
+
{exception.employee_name}
+
{exception.employee_id}
+
+
+ + + {typeLabels[exception.exception_type] || typeLabels[exception.exception_type.toLowerCase()] || typeLabels[exception.exception_type.toUpperCase()] || exception.exception_type} + + + +
+
+ + {severityConfig[exception.severity]?.label || exception.severity} - - -
-
{exception.employee_name}
-
{exception.employee_id}
-
-
- - - {typeLabels[exception.exception_type] || typeLabels[exception.exception_type.toLowerCase()] || typeLabels[exception.exception_type.toUpperCase()] || exception.exception_type} - - - -
-
- - {severityConfig[exception.severity]?.label || exception.severity} - -
- - - {exception.description} - - - {exception.difference_amount != null && ( - 0 ? "text-red-600" : "text-emerald-600"}> - {exception.difference_amount > 0 ? "+" : ""} - {exception.difference_amount.toFixed(2)} +
+
+ + {exception.description} + + + {exception.difference_amount != null && ( + 0 ? "text-red-600" : "text-emerald-600"}> + {exception.difference_amount > 0 ? "+" : ""} + {exception.difference_amount.toFixed(2)} )} @@ -518,9 +512,8 @@ function ExceptionsContent() {
- - )) - )} + )) + )} @@ -608,7 +601,16 @@ function ExceptionsContent() {

详细信息

-                        {JSON.stringify(JSON.parse(selectedException.detail), null, 2)}
+                        {(() => {
+                          try {
+                            const detail = typeof selectedException.detail === 'string' 
+                              ? JSON.parse(selectedException.detail) 
+                              : selectedException.detail;
+                            return JSON.stringify(detail, null, 2);
+                          } catch {
+                            return String(selectedException.detail);
+                          }
+                        })()}
                       
)} diff --git a/frontend/app/(dashboard)/settings/rules/page.tsx b/frontend/app/(dashboard)/settings/rules/page.tsx index 693cc7f..32e698c 100644 --- a/frontend/app/(dashboard)/settings/rules/page.tsx +++ b/frontend/app/(dashboard)/settings/rules/page.tsx @@ -1,10 +1,8 @@ "use client"; import { useState, useCallback } from "react"; -import { useRouter } from "next/navigation"; import { motion } from "motion/react"; import { - ArrowLeft, Trash2, ToggleLeft, ToggleRight, @@ -93,7 +91,6 @@ const item = { }; export default function RulesPage() { - const router = useRouter(); const toast = useToast(); const [rules, setRules] = useState(mockRules); @@ -160,28 +157,19 @@ export default function RulesPage() { const inactiveCount = rules.filter((r) => r.status === "INACTIVE").length; return ( -
+
{/* 页面头部 */} -
- -
-

规则管理

-

- 管理企业字段映射规则,实现自动识别 -

-
+
+

规则管理

+

+ 管理企业字段映射规则,实现自动识别 +

diff --git a/frontend/app/(dashboard)/tasks/[id]/mapping/page.tsx b/frontend/app/(dashboard)/tasks/[id]/mapping/page.tsx index 8f5061d..49b7a39 100644 --- a/frontend/app/(dashboard)/tasks/[id]/mapping/page.tsx +++ b/frontend/app/(dashboard)/tasks/[id]/mapping/page.tsx @@ -439,12 +439,12 @@ export default function MappingPage() { [toast] ); - // 确认并进入下一步 + // 确认并进入下一步 - 跳转到对账结果页面 const handleConfirmAndNext = useCallback(async () => { try { setLoading(true); toast.success("字段映射已确认,正在进入对账..."); - router.push(`/tasks/${taskId}/reconciliation`); + router.push(`/tasks/${taskId}/result`); } catch { toast.error("操作失败"); } finally { @@ -475,7 +475,7 @@ export default function MappingPage() { className="flex items-center justify-between" >
-
diff --git a/frontend/app/(dashboard)/tasks/page.tsx b/frontend/app/(dashboard)/tasks/page.tsx index b683569..f20dfa4 100644 --- a/frontend/app/(dashboard)/tasks/page.tsx +++ b/frontend/app/(dashboard)/tasks/page.tsx @@ -82,14 +82,13 @@ export default function TasksPage() { useEffect(() => { loadTasks(); - }, [loadTasks]); + }, [loadTasks, page, pageSize, selectedPeriod]); function onPageChange(newPage: number, newPageSize: number) { setPage(newPage); if (newPageSize !== pageSize) { setPageSize(newPageSize); } - loadTasks(); } const getMatchRate = (task: Task) => { @@ -99,7 +98,7 @@ export default function TasksPage() { }; return ( -
+
{/* Header */}