"use client"; import { useState, useCallback } from "react"; import { useParams, useRouter } from "next/navigation"; import { motion } from "motion/react"; import { ArrowLeft, CheckCircle, Loader2, FileSpreadsheet } from "lucide-react"; import { api } from "@/lib/api/client"; import { ENDPOINTS } from "@/lib/api/endpoints"; import { useToast } from "@/lib/hooks/useToast"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { FieldMappingTable } from "@/components/mapping/FieldMappingTable"; import { AISuggestionPanel } from "@/components/mapping/AISuggestionPanel"; import type { TaskMappingOverview } from "@/lib/api/types"; // 模拟数据(实际应从 API 获取) const mockMappingData: TaskMappingOverview = { task_id: 1, total_files: 3, total_fields: 18, confirmed_fields: 5, needs_review: true, files: [ { file_id: 1, file_type: "工资表", file_name: "2024年1月工资表.xlsx", total_fields: 6, confirmed_fields: 2, high_confidence: 4, medium_confidence: 1, low_confidence: 1, mappings: [ { id: 1, company_id: 1, file_id: 1, source_field: "员工姓名", standard_field: "员工姓名", confidence: 1.0, reasoning: "字段名完全匹配", sample_values: ["张三", "李四", "王五"], is_skipped: false, confirmed: true, confirmed_by: 1, confirmed_at: new Date().toISOString(), }, { id: 2, company_id: 1, file_id: 1, source_field: "工号", standard_field: "工号", confidence: 1.0, reasoning: "字段名完全匹配", sample_values: ["EMP001", "EMP002"], is_skipped: false, confirmed: true, confirmed_by: 1, confirmed_at: new Date().toISOString(), }, { id: 3, company_id: 1, file_id: 1, source_field: "基本薪资", standard_field: "基本工资", confidence: 0.85, reasoning: "字段名相似,样例值验证为数值类型", sample_values: [8000, 10000, 12000], is_skipped: false, confirmed: false, }, { id: 4, company_id: 1, file_id: 1, source_field: "岗位津贴", standard_field: "补贴", confidence: 0.75, reasoning: "字段名包含关键词:'津贴'", sample_values: [500, 800, 1000], is_skipped: false, confirmed: false, }, { id: 5, company_id: 1, file_id: 1, source_field: "实发合计", standard_field: "实发工资", confidence: 0.6, reasoning: "字段名部分匹配,需要确认", sample_values: [8500, 10800, 13000], is_skipped: false, confirmed: false, }, { id: 6, company_id: 1, file_id: 1, source_field: "备注", standard_field: "", confidence: 0.0, reasoning: "无法识别字段类型", sample_values: ["正常", "补发"], is_skipped: false, confirmed: false, }, ], }, { file_id: 2, file_type: "社保表", file_name: "2024年1月社保表.xlsx", total_fields: 6, confirmed_fields: 2, high_confidence: 3, medium_confidence: 2, low_confidence: 1, mappings: [ { id: 7, company_id: 1, file_id: 2, source_field: "姓名", standard_field: "员工姓名", confidence: 0.95, reasoning: "字段名包含关键词:'姓名'", sample_values: ["张三", "李四"], is_skipped: false, confirmed: true, }, { id: 8, company_id: 1, file_id: 2, source_field: "养老保险", standard_field: "养老保险", confidence: 1.0, reasoning: "字段名完全匹配", sample_values: [800, 960, 1200], is_skipped: false, confirmed: true, }, { id: 9, company_id: 1, file_id: 2, source_field: "医疗", standard_field: "医疗保险", confidence: 0.8, reasoning: "字段名部分匹配", sample_values: [200, 240, 300], is_skipped: false, confirmed: false, }, { id: 10, company_id: 1, file_id: 2, source_field: "失业", standard_field: "失业保险", confidence: 0.75, reasoning: "字段名相似", sample_values: [50, 60, 75], is_skipped: false, confirmed: false, }, { id: 11, company_id: 1, file_id: 2, source_field: "公积金", standard_field: "公积金", confidence: 0.5, reasoning: "需要确认具体含义", sample_values: [500, 600, 750], is_skipped: false, confirmed: false, }, { id: 12, company_id: 1, file_id: 2, source_field: "合计", standard_field: "社保合计", confidence: 0.6, reasoning: "可能是合计字段", sample_values: [1550, 1800, 2325], is_skipped: false, confirmed: false, }, ], }, { file_id: 3, file_type: "个税表", file_name: "2024年1月个税表.xlsx", total_fields: 6, confirmed_fields: 1, high_confidence: 2, medium_confidence: 3, low_confidence: 1, mappings: [ { id: 13, company_id: 1, file_id: 3, source_field: "姓名", standard_field: "员工姓名", confidence: 0.95, reasoning: "字段名包含关键词:'姓名'", sample_values: ["张三", "李四"], is_skipped: false, confirmed: true, }, { id: 14, company_id: 1, file_id: 3, source_field: "应税工资", standard_field: "应税收入", confidence: 0.9, reasoning: "字段名包含关键词:'应税'", sample_values: [8000, 10000], is_skipped: false, confirmed: false, }, { id: 15, company_id: 1, file_id: 3, source_field: "专项扣除", standard_field: "税前扣除", confidence: 0.8, reasoning: "可能为税前扣除项", sample_values: [1000, 1500], is_skipped: false, confirmed: false, }, { id: 16, company_id: 1, file_id: 3, source_field: "个税", standard_field: "应缴个税", confidence: 0.7, reasoning: "可能为个税字段", sample_values: [200, 300], is_skipped: false, confirmed: false, }, { id: 17, company_id: 1, file_id: 3, source_field: "税后工资", standard_field: "税后收入", confidence: 0.85, reasoning: "字段名相似", sample_values: [6800, 8500], is_skipped: false, confirmed: false, }, { id: 18, company_id: 1, file_id: 3, source_field: "银行账号", standard_field: "银行账号", confidence: 1.0, reasoning: "字段名完全匹配", sample_values: ["6222021234567890"], is_skipped: false, confirmed: false, }, ], }, ], }; const container = { hidden: { opacity: 0 }, show: { opacity: 1, transition: { staggerChildren: 0.08 }, }, }; const item = { hidden: { opacity: 0, y: 12 }, show: { opacity: 1, y: 0, transition: { duration: 0.4, ease: [0.16, 1, 0.3, 1] as const } }, }; export default function MappingPage() { const params = useParams(); const router = useRouter(); const toast = useToast(); const taskId = params.id as string; const [data, setData] = useState(mockMappingData); const [loading, setLoading] = useState(false); const [activeFile, setActiveFile] = useState(mockMappingData?.files[0]?.file_id || 1); // 统计 const totalHigh = data?.files.reduce((acc, f) => acc + f.high_confidence, 0) || 0; const totalMedium = data?.files.reduce((acc, f) => acc + f.medium_confidence, 0) || 0; const totalLow = data?.files.reduce((acc, f) => acc + f.low_confidence, 0) || 0; // 更新映射 const handleUpdateMapping = useCallback( async (mappingId: number, standardField: string) => { try { await api.put(ENDPOINTS.MAPPING.UPDATE(mappingId), { standard_field: standardField, }); setData((prev) => { if (!prev) return prev; const newFiles = prev.files.map((file) => ({ ...file, mappings: file.mappings.map((m) => m.id === mappingId ? { ...m, standard_field: standardField, confidence: 1.0 } : m ), })); return { ...prev, files: newFiles }; }); toast.success("字段已更新"); } catch { toast.error("更新失败"); } }, [toast] ); // 跳过映射 const handleSkipMapping = useCallback( async (mappingId: number) => { try { await api.put(ENDPOINTS.MAPPING.UPDATE(mappingId), { is_skipped: true, }); setData((prev) => { if (!prev) return prev; const newFiles = prev.files.map((file) => ({ ...file, mappings: file.mappings.map((m) => m.id === mappingId ? { ...m, is_skipped: true } : m ), })); return { ...prev, files: newFiles }; }); toast.success("字段已跳过"); } catch { toast.error("操作失败"); } }, [toast] ); // 确认单个映射 const handleConfirmMapping = useCallback( async (mappingId: number) => { try { await api.post(ENDPOINTS.MAPPING.CONFIRM, { mapping_ids: [mappingId], }); setData((prev) => { if (!prev) return prev; const newFiles = prev.files.map((file) => ({ ...file, mappings: file.mappings.map((m) => m.id === mappingId ? { ...m, confirmed: true, confirmed_at: new Date().toISOString() } : m ), confirmed_fields: file.mappings.filter( (m) => m.id === mappingId || m.confirmed ).length, })); const totalConfirmed = newFiles.reduce((acc, f) => acc + f.confirmed_fields, 0); return { ...prev, files: newFiles, confirmed_fields: totalConfirmed }; }); toast.success("字段已确认"); } catch { toast.error("确认失败"); } }, [toast] ); // 批量确认 const handleConfirmAll = useCallback( async (mappingIds: number[]) => { if (mappingIds.length === 0) return; try { setLoading(true); await api.post(ENDPOINTS.MAPPING.CONFIRM, { mapping_ids: mappingIds, }); setData((prev) => { if (!prev) return prev; const newFiles = prev.files.map((file) => { const fileIds = file.mappings.map((m) => m.id); const confirmedIds = mappingIds.filter((id) => fileIds.includes(id)); return { ...file, mappings: file.mappings.map((m) => confirmedIds.includes(m.id) ? { ...m, confirmed: true, confirmed_at: new Date().toISOString() } : m ), confirmed_fields: file.mappings.filter( (m) => m.confirmed || confirmedIds.includes(m.id) ).length, }; }); const totalConfirmed = newFiles.reduce((acc, f) => acc + f.confirmed_fields, 0); return { ...prev, files: newFiles, confirmed_fields: totalConfirmed }; }); toast.success(`已确认 ${mappingIds.length} 个字段`); } catch { toast.error("批量确认失败"); } finally { setLoading(false); } }, [toast] ); // 确认并进入下一步 const handleConfirmAndNext = useCallback(async () => { try { setLoading(true); toast.success("字段映射已确认,正在进入对账..."); router.push(`/tasks/${taskId}/reconciliation`); } catch { toast.error("操作失败"); } finally { setLoading(false); } }, [taskId, router, toast]); const currentFile = data?.files.find((f) => f.file_id === activeFile); if (!data) { return (
加载中...
); } return (
{/* 页面头部 */}

字段映射确认

任务 #{taskId} - 确认 AI 识别的字段映射关系

{/* AI 建议面板 */} { const ids = data.files .flatMap((f) => f.mappings) .filter((m) => !m.confirmed && !m.is_skipped && m.standard_field) .map((m) => m.id); handleConfirmAll(ids); }} /> {/* 文件标签页 */}
{data.files.map((file) => ( ))}
{/* 字段映射表格 */} {currentFile && ( )} {/* 文件概览卡片 */} {data.files.map((file) => ( setActiveFile(file.file_id)} >

{file.file_type}

{file.file_name}

已确认 {file.confirmed_fields}/{file.total_fields} {file.low_confidence > 0 && ( {file.low_confidence} 个待检查 )}
{/* 置信度进度条 */}
))}
); }