feat: 完善前后端核心功能模块
后端: - 新增认证(auth)、任务(tasks)、映射(mappings)、对账(reconciliation)、异常(exceptions)、导出(exports) API - 新增核心模块: database, security, permissions, tenant, exceptions, error_handlers - 新增数据模型: user, company, reconciliation_task, field_mapping, uploaded_file 等 - 新增服务层: ai_recognizer, file_parser, file_storage, mapping, reconciliation 等 - 添加数据库迁移脚本 前端: - 新增登录页面和仪表盘页面 - 新增任务列表、任务详情、字段映射页面 - 新增异常处理页面和规则设置页面 - 新增 API 代理路由 /api/[...path] - 新增 UI 组件库 (button, card, dialog, input, table 等) - 新增 auth 组件 (ProtectedRoute, PermissionGate) - 新增 layout 组件 (Header, Sidebar) - 新增 mapping 组件 (FieldMappingTable, AISuggestionPanel) - 新增 API 客户端和 hooks (useAsync, useToast, usePermission 等) - 新增状态管理 (auth-store, company-store, ui-store) - 集成 Tailwind CSS 和 shadcn/ui 组件库 其他: - 添加 Alembic 数据库迁移配置 - 添加初始化示例数据脚本 - 更新项目文档
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "motion/react";
|
||||
import { AlertTriangle, CheckCircle, Info, Sparkles } from "lucide-react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AISuggestionPanelProps {
|
||||
totalFields: number;
|
||||
highConfidence: number;
|
||||
mediumConfidence: number;
|
||||
lowConfidence: number;
|
||||
confirmedCount: number;
|
||||
onAutoConfirm?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const config = {
|
||||
success: {
|
||||
bg: "bg-emerald-50 dark:bg-emerald-950/30",
|
||||
border: "border-emerald-200 dark:border-emerald-800",
|
||||
iconBg: "bg-emerald-100 dark:bg-emerald-900/50",
|
||||
iconColor: "text-emerald-600 dark:text-emerald-400",
|
||||
titleColor: "text-emerald-800 dark:text-emerald-300",
|
||||
descColor: "text-emerald-700 dark:text-emerald-400",
|
||||
buttonBg: "bg-emerald-600 hover:bg-emerald-700",
|
||||
progressBg: "bg-emerald-500",
|
||||
},
|
||||
warning: {
|
||||
bg: "bg-amber-50 dark:bg-amber-950/30",
|
||||
border: "border-amber-200 dark:border-amber-800",
|
||||
iconBg: "bg-amber-100 dark:bg-amber-900/50",
|
||||
iconColor: "text-amber-600 dark:text-amber-400",
|
||||
titleColor: "text-amber-800 dark:text-amber-300",
|
||||
descColor: "text-amber-700 dark:text-amber-400",
|
||||
buttonBg: "bg-amber-600 hover:bg-amber-700",
|
||||
progressBg: "bg-amber-500",
|
||||
},
|
||||
info: {
|
||||
bg: "bg-blue-50 dark:bg-blue-950/30",
|
||||
border: "border-blue-200 dark:border-blue-800",
|
||||
iconBg: "bg-blue-100 dark:bg-blue-900/50",
|
||||
iconColor: "text-blue-600 dark:text-blue-400",
|
||||
titleColor: "text-blue-800 dark:text-blue-300",
|
||||
descColor: "text-blue-700 dark:text-blue-400",
|
||||
buttonBg: "bg-blue-600 hover:bg-blue-700",
|
||||
progressBg: "bg-blue-500",
|
||||
},
|
||||
};
|
||||
|
||||
export function AISuggestionPanel({
|
||||
totalFields,
|
||||
highConfidence,
|
||||
mediumConfidence,
|
||||
lowConfidence,
|
||||
confirmedCount,
|
||||
onAutoConfirm,
|
||||
className,
|
||||
}: AISuggestionPanelProps) {
|
||||
const pendingCount = totalFields - confirmedCount;
|
||||
const allConfirmed = pendingCount === 0;
|
||||
const progressPercent = totalFields > 0 ? (confirmedCount / totalFields) * 100 : 0;
|
||||
|
||||
const getSuggestion = () => {
|
||||
if (allConfirmed) {
|
||||
return {
|
||||
type: "success" as const,
|
||||
icon: CheckCircle,
|
||||
title: "所有字段已确认",
|
||||
description: "字段映射已完成,可以进入下一步",
|
||||
action: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (lowConfidence > 0) {
|
||||
return {
|
||||
type: "warning" as const,
|
||||
icon: AlertTriangle,
|
||||
title: "建议检查低置信度字段",
|
||||
description: `有 ${lowConfidence} 个字段置信度较低(<70%),建议人工确认`,
|
||||
action: "检查低置信度字段",
|
||||
};
|
||||
}
|
||||
|
||||
if (mediumConfidence > 0) {
|
||||
return {
|
||||
type: "info" as const,
|
||||
icon: Info,
|
||||
title: "存在中等置信度字段",
|
||||
description: `有 ${mediumConfidence} 个字段置信度为中等(70%-90%),建议确认`,
|
||||
action: "批量确认",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: "success" as const,
|
||||
icon: Sparkles,
|
||||
title: "可直接确认",
|
||||
description: "所有字段置信度较高,可以批量确认",
|
||||
action: "批量确认",
|
||||
};
|
||||
};
|
||||
|
||||
const suggestion = getSuggestion();
|
||||
const Icon = suggestion.icon;
|
||||
const style = config[suggestion.type];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.98 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] as const }}
|
||||
>
|
||||
<Card className={cn(style.bg, style.border, className)}>
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Icon */}
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ delay: 0.1, type: "spring", stiffness: 200 }}
|
||||
className={cn("p-2.5 rounded-xl", style.iconBg)}
|
||||
>
|
||||
<Icon className={cn("w-5 h-5", style.iconColor)} />
|
||||
</motion.div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h3 className={cn("font-semibold text-base", style.titleColor)}>
|
||||
{suggestion.title}
|
||||
</h3>
|
||||
<p className={cn("text-sm mt-1", style.descColor)}>
|
||||
{suggestion.description}
|
||||
</p>
|
||||
|
||||
{suggestion.action && !allConfirmed && onAutoConfirm && (
|
||||
<Button
|
||||
onClick={onAutoConfirm}
|
||||
size="sm"
|
||||
className={cn("mt-3 text-white btn-press", style.buttonBg)}
|
||||
>
|
||||
{suggestion.action}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress Stats */}
|
||||
<div className="text-right shrink-0">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="text-3xl font-bold text-foreground"
|
||||
>
|
||||
{confirmedCount}
|
||||
<span className="text-lg text-muted-foreground">/{totalFields}</span>
|
||||
</motion.div>
|
||||
<p className="text-caption text-muted-foreground">已确认</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mt-5">
|
||||
<div className="flex justify-between text-caption text-muted-foreground mb-1.5">
|
||||
<span>确认进度</span>
|
||||
<span>{Math.round(progressPercent)}%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${progressPercent}%` }}
|
||||
transition={{ duration: 0.6, delay: 0.3, ease: [0.16, 1, 0.3, 1] as const }}
|
||||
className={cn("h-full rounded-full", style.progressBg)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confidence Distribution */}
|
||||
<div className="mt-5 pt-4 border-t border-border/50">
|
||||
<p className="text-caption text-muted-foreground mb-3">置信度分布</p>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-emerald-500" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
高置信度: <span className="font-semibold text-foreground">{highConfidence}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-amber-500" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
中置信度: <span className="font-semibold text-foreground">{mediumConfidence}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-red-500" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
低置信度: <span className="font-semibold text-foreground">{lowConfidence}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AISuggestionPanel;
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ConfidenceBadgeProps {
|
||||
confidence: number;
|
||||
showLabel?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ConfidenceBadge({
|
||||
confidence,
|
||||
showLabel = true,
|
||||
className,
|
||||
}: ConfidenceBadgeProps) {
|
||||
const getLevel = (value: number) => {
|
||||
if (value > 0.9) return "high";
|
||||
if (value >= 0.7) return "medium";
|
||||
return "low";
|
||||
};
|
||||
|
||||
const getLabel = (value: number) => {
|
||||
if (value > 0.9) return "高";
|
||||
if (value >= 0.7) return "中";
|
||||
return "低";
|
||||
};
|
||||
|
||||
const level = getLevel(confidence);
|
||||
const label = getLabel(confidence);
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium",
|
||||
level === "high" && "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
|
||||
level === "medium" && "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300",
|
||||
level === "low" && "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{showLabel ? label : `${(confidence * 100).toFixed(0)}%`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfidenceBadge;
|
||||
@@ -0,0 +1,316 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { ChevronDown, ChevronRight, Eye, Check } from "lucide-react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ConfidenceBadge } from "./ConfidenceBadge";
|
||||
import type { FieldMappingResponse } from "@/lib/api/types";
|
||||
|
||||
interface FieldMappingTableProps {
|
||||
mappings: FieldMappingResponse[];
|
||||
fileName: string;
|
||||
fileType: string;
|
||||
onUpdate?: (mappingId: number, standardField: string) => void;
|
||||
onSkip?: (mappingId: number) => void;
|
||||
onConfirm?: (mappingId: number) => void;
|
||||
onConfirmAll?: (mappingIds: number[]) => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
const STANDARD_FIELDS = [
|
||||
{ value: "员工姓名", label: "员工姓名" },
|
||||
{ value: "工号", label: "工号" },
|
||||
{ value: "部门", label: "部门" },
|
||||
{ value: "岗位", label: "岗位" },
|
||||
{ value: "基本工资", label: "基本工资" },
|
||||
{ value: "奖金", label: "奖金" },
|
||||
{ value: "补贴", label: "补贴" },
|
||||
{ value: "加班费", label: "加班费" },
|
||||
{ value: "扣款", label: "扣款" },
|
||||
{ value: "应发工资", label: "应发工资" },
|
||||
{ value: "实发工资", label: "实发工资" },
|
||||
{ value: "银行账号", label: "银行账号" },
|
||||
{ value: "身份证号", label: "身份证号" },
|
||||
{ value: "社保基数", label: "社保基数" },
|
||||
{ value: "养老保险", label: "养老保险" },
|
||||
{ value: "医疗保险", label: "医疗保险" },
|
||||
{ value: "失业保险", label: "失业保险" },
|
||||
{ value: "公积金", label: "公积金" },
|
||||
{ value: "个税", label: "个税" },
|
||||
{ value: "应税收入", label: "应税收入" },
|
||||
{ value: "税后收入", label: "税后收入" },
|
||||
];
|
||||
|
||||
export function FieldMappingTable({
|
||||
mappings,
|
||||
fileName,
|
||||
fileType,
|
||||
onUpdate,
|
||||
onSkip,
|
||||
onConfirm,
|
||||
onConfirmAll,
|
||||
readOnly = false,
|
||||
}: FieldMappingTableProps) {
|
||||
const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [editValue, setEditValue] = useState("");
|
||||
|
||||
const toggleExpand = (id: number) => {
|
||||
const newExpanded = new Set(expandedRows);
|
||||
if (newExpanded.has(id)) {
|
||||
newExpanded.delete(id);
|
||||
} else {
|
||||
newExpanded.add(id);
|
||||
}
|
||||
setExpandedRows(newExpanded);
|
||||
};
|
||||
|
||||
const startEdit = (id: number, currentValue: string) => {
|
||||
setEditingId(id);
|
||||
setEditValue(currentValue);
|
||||
};
|
||||
|
||||
const saveEdit = (id: number) => {
|
||||
if (onUpdate && editValue) {
|
||||
onUpdate(id, editValue);
|
||||
}
|
||||
setEditingId(null);
|
||||
setEditValue("");
|
||||
};
|
||||
|
||||
const handleConfirmAll = () => {
|
||||
if (onConfirmAll) {
|
||||
const unconfirmedIds = mappings
|
||||
.filter((m) => !m.confirmed && !m.is_skipped && m.standard_field)
|
||||
.map((m) => m.id);
|
||||
onConfirmAll(unconfirmedIds);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmedCount = mappings.filter((m) => m.confirmed).length;
|
||||
const unconfirmedCount = mappings.length - confirmedCount;
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
{/* 表头 */}
|
||||
<div className="bg-muted/50 px-5 py-4 border-b border-border">
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-sm font-medium text-foreground">{fileName}</div>
|
||||
<Badge variant="secondary">{fileType}</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
已确认: <span className="font-semibold text-emerald-600">{confirmedCount}</span>
|
||||
</span>
|
||||
<span className="text-muted-foreground">
|
||||
待确认: <span className="font-semibold text-amber-600">{unconfirmedCount}</span>
|
||||
</span>
|
||||
{!readOnly && unconfirmedCount > 0 && (
|
||||
<Button onClick={handleConfirmAll} size="sm" className="btn-press">
|
||||
<Check className="w-4 h-4 mr-1" />
|
||||
批量确认
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 表格 */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-muted/30">
|
||||
<th className="px-4 py-3 text-left font-medium text-muted-foreground w-10" />
|
||||
<th className="px-4 py-3 text-left font-medium text-muted-foreground">
|
||||
源字段
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-medium text-muted-foreground">
|
||||
标准字段
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center font-medium text-muted-foreground w-24">
|
||||
置信度
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center font-medium text-muted-foreground w-20">
|
||||
状态
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right font-medium text-muted-foreground w-36">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{mappings.map((mapping, index) => (
|
||||
<motion.tr
|
||||
key={mapping.id}
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.2, delay: index * 0.03 }}
|
||||
className={cn(
|
||||
"group hover:bg-muted/30 transition-colors",
|
||||
mapping.is_skipped && "opacity-50",
|
||||
mapping.confirmed && "bg-emerald-50/30 dark:bg-emerald-950/10"
|
||||
)}
|
||||
>
|
||||
<td className="px-4 py-3">
|
||||
<button
|
||||
onClick={() => toggleExpand(mapping.id)}
|
||||
className="p-1 hover:bg-muted rounded-md transition-colors"
|
||||
>
|
||||
{expandedRows.has(mapping.id) ? (
|
||||
<ChevronDown className="w-4 h-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-4 py-3 font-medium text-foreground">
|
||||
{mapping.source_field}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{editingId === mapping.id ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
className="flex-1 px-2 py-1.5 border border-input rounded-md text-sm bg-background focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
>
|
||||
<option value="">选择标准字段</option>
|
||||
{STANDARD_FIELDS.map((field) => (
|
||||
<option key={field.value} value={field.value}>
|
||||
{field.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => saveEdit(mapping.id)}
|
||||
className="h-7 w-7 p-0 text-emerald-600 hover:text-emerald-700 hover:bg-emerald-50 dark:hover:bg-emerald-950/30"
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
) : mapping.standard_field ? (
|
||||
<span className="text-foreground">{mapping.standard_field}</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground italic">未识别</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<ConfidenceBadge confidence={mapping.confidence} />
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
{mapping.is_skipped ? (
|
||||
<Badge variant="secondary" className="text-xs">已跳过</Badge>
|
||||
) : mapping.confirmed ? (
|
||||
<Badge className="bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300 text-xs font-medium">
|
||||
已确认
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-xs">待确认</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{!readOnly && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="flex items-center justify-end gap-1"
|
||||
>
|
||||
{editingId !== mapping.id && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => startEdit(mapping.id, mapping.standard_field)}
|
||||
className="h-7 px-2 text-xs text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:hover:bg-blue-950/30"
|
||||
>
|
||||
修改
|
||||
</Button>
|
||||
{!mapping.confirmed && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onConfirm?.(mapping.id)}
|
||||
className="h-7 px-2 text-xs text-emerald-600 hover:text-emerald-700 hover:bg-emerald-50 dark:hover:bg-emerald-950/30"
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onSkip?.(mapping.id)}
|
||||
className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
跳过
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</td>
|
||||
</motion.tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{mappings.length === 0 && (
|
||||
<div className="px-4 py-12 text-center">
|
||||
<p className="text-muted-foreground">暂无字段映射数据</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 展开详情 */}
|
||||
<AnimatePresence>
|
||||
{Array.from(expandedRows).map((id) => {
|
||||
const mapping = mappings.find((m) => m.id === id);
|
||||
if (!mapping || !mapping.sample_values) return null;
|
||||
return (
|
||||
<motion.div
|
||||
key={`detail-${id}`}
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="px-4 py-4 bg-muted/30 border-t border-border">
|
||||
<div className="space-y-3 max-w-2xl">
|
||||
{mapping.reasoning && (
|
||||
<div className="text-sm">
|
||||
<span className="font-medium text-muted-foreground">判断依据: </span>
|
||||
<span className="text-foreground">{mapping.reasoning}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className="text-sm font-medium text-muted-foreground">样例值: </span>
|
||||
<div className="mt-1.5 flex flex-wrap gap-2">
|
||||
{(mapping.sample_values as unknown as string[]).slice(0, 5).map((value, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-background border border-border rounded-md text-xs text-foreground"
|
||||
>
|
||||
<Eye className="w-3 h-3 text-muted-foreground" />
|
||||
{String(value)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default FieldMappingTable;
|
||||
Reference in New Issue
Block a user