"use client"; import { useState, useEffect, use, useCallback, useRef } from "react"; import { useRouter } from "next/navigation"; import { motion } from "motion/react"; import { api } from "@/lib/api/client"; import { Card, CardContent, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/components/ui/tabs"; import { CheckCircle, FileText, Download, RefreshCw, ArrowLeft, TrendingUp, Users, AlertOctagon, } from "lucide-react"; interface TaskDetail { id: number; name: string; period: string; status: string; total_employees: number; matched_count: number; exception_count: number; created_at: string; completed_at: string | null; } interface ReconciliationResult { task_id: number; period: string; total_employees: number; matched_employees: number; exception_count: number; exceptions_by_type: Record; exceptions_by_severity: Record; execution_time_ms: number; completed_at: string; } interface ExceptionItem { id: number; exception_type: string; severity: "low" | "medium" | "high" | "critical"; status: string; employee_id: string; employee_name: string; description: string; salary_amount: number | null; social_security_amount: number | null; tax_amount: number | null; bank_amount: number | null; difference_amount: number | null; } const severityConfig = { low: { label: "低", bg: "bg-blue-100 dark:bg-blue-900/40", text: "text-blue-700 dark:text-blue-300" }, medium: { label: "中", bg: "bg-yellow-100 dark:bg-yellow-900/40", text: "text-yellow-700 dark:text-yellow-300" }, high: { label: "高", bg: "bg-orange-100 dark:bg-orange-900/40", text: "text-orange-700 dark:text-orange-300" }, critical: { label: "严重", bg: "bg-red-100 dark:bg-red-900/40", text: "text-red-700 dark:text-red-300" }, }; const typeLabels: Record = { amount_mismatch: "金额不一致", missing_record: "记录缺失", duplicate_record: "重复记录", format_error: "格式错误", logic_error: "逻辑错误", rule_violation: "规则违规", threshold_exceeded: "阈值超限", }; export default function TaskResultPage({ params }: { params: Promise<{ id: string }> }) { const { id: taskId } = use(params); const router = useRouter(); const [task, setTask] = useState(null); const [result, setResult] = useState(null); const [exceptions, setExceptions] = useState([]); const [loading, setLoading] = useState(true); const fetchResultRef = useRef<() => Promise>(); const fetchTaskDetail = useCallback(async () => { try { const res = await api.get(`/api/tasks/${taskId}`); setTask(res.data); if (res.data.status === "COMPLETED" && fetchResultRef.current) { fetchResultRef.current(); } } catch (error) { console.error("获取任务详情失败", error); } finally { setLoading(false); } }, [taskId]); const fetchResult = useCallback(async () => { try { const res = await api.get(`/api/reconciliation/result/${taskId}`); setResult(res.data); } catch (error) { console.error("获取对账结果失败", error); } }, [taskId]); // Store fetchResult in ref to avoid circular dependency useEffect(() => { fetchResultRef.current = fetchResult; }, [fetchResult]); const fetchExceptions = useCallback(async () => { try { const res = await api.get<{ items: ExceptionItem[] }>(`/api/exceptions/`, { params: { task_id: taskId, page_size: 100 }, }); setExceptions(res.data.items || []); } catch (error) { console.error("获取异常列表失败", error); } }, [taskId]); useEffect(() => { if (taskId) { fetchTaskDetail(); fetchExceptions(); } }, [taskId, fetchTaskDetail, fetchExceptions]); async function handleExport() { try { const res = await api.get(`/api/exports/task/${taskId}`, { responseType: "blob", }); const url = window.URL.createObjectURL(res.data); const a = document.createElement("a"); a.href = url; a.download = `对账结果_${task?.period || taskId}.xlsx`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } catch (error) { console.error("导出失败", error); } } async function handleExportKingdee() { try { const res = await api.get(`/api/exports/kingdee/${taskId}`, { responseType: "blob", }); const url = window.URL.createObjectURL(res.data); const a = document.createElement("a"); a.href = url; a.download = `金蝶凭证_${task?.period || taskId}.xlsx`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } catch (error) { console.error("导出金蝶凭证失败", error); } } if (loading) { return (
加载中...
); } if (!task) { return (

任务不存在

); } const matchRate = task.total_employees > 0 ? ((task.matched_count / task.total_employees) * 100).toFixed(1) : "0.0"; const container = { hidden: { opacity: 0 }, show: { opacity: 1, transition: { staggerChildren: 0.06 }, }, }; const item = { hidden: { opacity: 0, y: 8 }, show: { opacity: 1, y: 0, transition: { duration: 0.3, ease: [0.16, 1, 0.3, 1] as const } }, }; return (
{/* Header */}

{task.name}

对账期间: {task.period}

{/* Stats Cards */}

总人数

{task.total_employees}

匹配人数

{task.matched_count}

异常数量

{task.exception_count}

匹配率

{matchRate}%

{/* Progress bar */}
{/* Tabs */} 概览统计 异常列表 {task.exception_count > 0 && ( {task.exception_count} )} 已匹配列表 {/* Overview Tab */}
{/* Exception Types */} 异常类型分布 {result?.exceptions_by_type && Object.keys(result.exceptions_by_type).length > 0 ? (
{Object.entries(result.exceptions_by_type).map(([type, count]) => (
{typeLabels[type] || type} {count}
))}
) : (

暂无数据

)}
{/* Severity Distribution */} 严重程度分布 {result?.exceptions_by_severity && Object.keys(result.exceptions_by_severity).length > 0 ? (
{["critical", "high", "medium", "low"].map((severity) => { const count = result.exceptions_by_severity[severity] || 0; const config = severityConfig[severity as keyof typeof severityConfig]; return (
{config.label}: {count}
); })}
) : (

暂无数据

)}
{/* Execution Info */} 执行信息

创建时间

{new Date(task.created_at).toLocaleString()}

完成时间

{task.completed_at ? new Date(task.completed_at).toLocaleString() : "-"}

{result && ( <>

执行时间

{result.execution_time_ms?.toFixed(0) || '-'}ms

结果生成时间

{result.completed_at ? new Date(result.completed_at).toLocaleString() : "-"}

)}
{/* Exceptions Tab */}
异常列表 {exceptions.length} 条记录
{exceptions.length === 0 ? (

完美匹配!

所有记录都匹配成功

) : ( 员工 类型 严重程度 描述 差异金额 {exceptions.map((exc) => (
{exc.employee_name}
{exc.employee_id}
{typeLabels[exc.exception_type] || exc.exception_type} {severityConfig[exc.severity].label} {exc.description} {exc.difference_amount != null && ( 0 ? "text-red-600" : "text-emerald-600"}> {exc.difference_amount > 0 ? "+" : ""} {exc.difference_amount.toFixed(2)} )}
))}
)}
{/* Matched Tab */}
已匹配列表 {task.matched_count} 条记录

所有记录都匹配成功!

共 {task.matched_count} 条记录通过所有对账规则

); }