Files
freedakgmail 33b4c734aa 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 数据库迁移配置
- 添加初始化示例数据脚本
- 更新项目文档
2026-07-07 09:04:47 +08:00

213 lines
7.6 KiB
TypeScript

"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;