33b4c734aa
后端: - 新增认证(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 数据库迁移配置 - 添加初始化示例数据脚本 - 更新项目文档
397 lines
16 KiB
TypeScript
397 lines
16 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import { motion } from "motion/react";
|
|
import {
|
|
FileText,
|
|
AlertTriangle,
|
|
CheckCircle,
|
|
TrendingUp,
|
|
Clock,
|
|
ArrowRight,
|
|
Loader2
|
|
} from "lucide-react";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { useRouter } from "next/navigation";
|
|
import { api } from "@/lib/api/client";
|
|
import ENDPOINTS from "@/lib/api/endpoints";
|
|
|
|
interface TaskStats {
|
|
monthly_tasks: number;
|
|
pending_exceptions: number;
|
|
completed_tasks: number;
|
|
match_rate: number;
|
|
current_period: string;
|
|
}
|
|
|
|
interface Task {
|
|
id: number;
|
|
period: string;
|
|
status: string;
|
|
total_employees: number;
|
|
matched_count: number;
|
|
exception_count: number;
|
|
created_at: string;
|
|
completed_at: string | null;
|
|
}
|
|
|
|
const statusLabels: Record<string, { label: string; color: string }> = {
|
|
PENDING: { label: "待处理", color: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300" },
|
|
PROCESSING: { label: "处理中", color: "bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300" },
|
|
MAPPING_COMPLETED: { label: "映射完成", color: "bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300" },
|
|
COMPLETED: { label: "已完成", color: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300" },
|
|
FAILED: { label: "失败", color: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300" },
|
|
};
|
|
|
|
const container = {
|
|
hidden: { opacity: 0 },
|
|
show: {
|
|
opacity: 1,
|
|
transition: {
|
|
staggerChildren: 0.08,
|
|
delayChildren: 0.1,
|
|
},
|
|
},
|
|
};
|
|
|
|
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 DashboardPage() {
|
|
const router = useRouter();
|
|
const [stats, setStats] = useState<TaskStats | null>(null);
|
|
const [recentTasks, setRecentTasks] = useState<Task[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const loadDashboardData = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
// 并行获取统计数据和任务列表
|
|
const [statsRes, tasksRes] = await Promise.all([
|
|
api.get<TaskStats>(ENDPOINTS.TASK.STATS),
|
|
api.get<Task[]>(ENDPOINTS.TASK.LIST, { params: { limit: 5 } }),
|
|
]);
|
|
|
|
setStats(statsRes.data);
|
|
setRecentTasks(tasksRes.data);
|
|
} catch (error) {
|
|
console.error("加载数据失败:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
loadDashboardData();
|
|
}, [loadDashboardData]);
|
|
|
|
const formatPeriod = (period: string) => {
|
|
const [year, month] = period.split("-");
|
|
return `${year}年${parseInt(month)}月`;
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-full p-6 lg:p-8 max-w-7xl mx-auto">
|
|
{/* Header */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] as const }}
|
|
className="mb-8"
|
|
>
|
|
<h1 className="text-heading-1 text-foreground mb-2">工作台</h1>
|
|
<p className="text-muted-foreground text-body">
|
|
持续追踪您的对账任务
|
|
</p>
|
|
</motion.div>
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-20">
|
|
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Stats Grid */}
|
|
<motion.div
|
|
variants={container}
|
|
initial="hidden"
|
|
animate="show"
|
|
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8"
|
|
>
|
|
<motion.div variants={item}>
|
|
<Card className="relative overflow-hidden hover-lift">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<p className="text-caption text-muted-foreground uppercase tracking-wide mb-1">
|
|
本月任务
|
|
</p>
|
|
<p className="text-3xl font-semibold tracking-tight text-foreground">
|
|
{stats?.monthly_tasks ?? 0}
|
|
</p>
|
|
<div className="flex items-center gap-1 mt-2">
|
|
<Clock className="w-3 h-3 text-muted-foreground" />
|
|
<span className="text-caption text-muted-foreground">
|
|
{stats?.current_period ? formatPeriod(stats.current_period) : "-"}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="p-2.5 rounded-lg bg-blue-50 dark:bg-blue-950/30">
|
|
<FileText className="w-5 h-5 text-blue-600" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
<div className="absolute inset-x-0 bottom-0 h-0.5 bg-blue-50 dark:bg-blue-950/30 opacity-50" />
|
|
</Card>
|
|
</motion.div>
|
|
|
|
<motion.div variants={item}>
|
|
<Card className="relative overflow-hidden hover-lift">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<p className="text-caption text-muted-foreground uppercase tracking-wide mb-1">
|
|
待处理异常
|
|
</p>
|
|
<p className="text-3xl font-semibold tracking-tight text-foreground">
|
|
{stats?.pending_exceptions ?? 0}
|
|
</p>
|
|
<div className="flex items-center gap-1 mt-2">
|
|
<AlertTriangle className="w-3 h-3 text-amber-500" />
|
|
<span className="text-caption text-muted-foreground">
|
|
需要人工处理
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="p-2.5 rounded-lg bg-amber-50 dark:bg-amber-950/30">
|
|
<AlertTriangle className="w-5 h-5 text-amber-600" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
<div className="absolute inset-x-0 bottom-0 h-0.5 bg-amber-50 dark:bg-amber-950/30 opacity-50" />
|
|
</Card>
|
|
</motion.div>
|
|
|
|
<motion.div variants={item}>
|
|
<Card className="relative overflow-hidden hover-lift">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<p className="text-caption text-muted-foreground uppercase tracking-wide mb-1">
|
|
已完成
|
|
</p>
|
|
<p className="text-3xl font-semibold tracking-tight text-foreground">
|
|
{stats?.completed_tasks ?? 0}
|
|
</p>
|
|
<div className="flex items-center gap-1 mt-2">
|
|
<CheckCircle className="w-3 h-3 text-emerald-500" />
|
|
<span className="text-caption text-muted-foreground">
|
|
本月累计
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="p-2.5 rounded-lg bg-emerald-50 dark:bg-emerald-950/30">
|
|
<CheckCircle className="w-5 h-5 text-emerald-600" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
<div className="absolute inset-x-0 bottom-0 h-0.5 bg-emerald-50 dark:bg-emerald-950/30 opacity-50" />
|
|
</Card>
|
|
</motion.div>
|
|
|
|
<motion.div variants={item}>
|
|
<Card className="relative overflow-hidden hover-lift">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<p className="text-caption text-muted-foreground uppercase tracking-wide mb-1">
|
|
匹配率
|
|
</p>
|
|
<p className="text-3xl font-semibold tracking-tight text-foreground">
|
|
{stats?.match_rate ?? 0}%
|
|
</p>
|
|
<div className="flex items-center gap-1 mt-2">
|
|
<TrendingUp className="w-3 h-3 text-violet-500" />
|
|
<span className="text-caption text-muted-foreground">
|
|
本月平均
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="p-2.5 rounded-lg bg-violet-50 dark:bg-violet-950/30">
|
|
<TrendingUp className="w-5 h-5 text-violet-600" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
<div className="absolute inset-x-0 bottom-0 h-0.5 bg-violet-50 dark:bg-violet-950/30 opacity-50" />
|
|
</Card>
|
|
</motion.div>
|
|
</motion.div>
|
|
|
|
{/* Recent Tasks */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 12 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.3, ease: [0.16, 1, 0.3, 1] as const }}
|
|
className="mb-8"
|
|
>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-heading-3 text-foreground">最近任务</h2>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => router.push("/tasks")}
|
|
>
|
|
查看全部
|
|
<ArrowRight className="w-4 h-4 ml-1" />
|
|
</Button>
|
|
</div>
|
|
|
|
{recentTasks.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="p-8 text-center">
|
|
<div className="w-12 h-12 rounded-full bg-muted mx-auto mb-4 flex items-center justify-center">
|
|
<FileText className="w-6 h-6 text-muted-foreground" />
|
|
</div>
|
|
<p className="text-body text-muted-foreground mb-2">
|
|
暂无对账任务
|
|
</p>
|
|
<p className="text-caption text-muted-foreground">
|
|
创建您的第一个对账任务开始使用
|
|
</p>
|
|
<Button
|
|
className="mt-4 btn-press"
|
|
onClick={() => router.push("/tasks/new")}
|
|
>
|
|
新建任务
|
|
<ArrowRight className="w-4 h-4 ml-2" />
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{recentTasks.map((task, index) => (
|
|
<motion.div
|
|
key={task.id}
|
|
initial={{ opacity: 0, y: 8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{
|
|
duration: 0.3,
|
|
delay: 0.1 + index * 0.05,
|
|
}}
|
|
>
|
|
<Card
|
|
className="cursor-pointer hover-lift transition-all"
|
|
onClick={() => router.push(`/tasks/${task.id}/result`)}
|
|
>
|
|
<CardContent className="p-4 flex items-center justify-between">
|
|
<div className="flex items-center gap-4">
|
|
<div className="p-2.5 rounded-lg bg-muted">
|
|
<FileText className="w-5 h-5 text-muted-foreground" />
|
|
</div>
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<h3 className="font-medium text-foreground">
|
|
{formatPeriod(task.period)} 对账任务
|
|
</h3>
|
|
<span className={`px-2 py-0.5 rounded text-xs font-medium ${statusLabels[task.status]?.color || statusLabels.PENDING.color}`}>
|
|
{statusLabels[task.status]?.label || task.status}
|
|
</span>
|
|
</div>
|
|
<p className="text-caption text-muted-foreground">
|
|
{task.total_employees} 名员工 · {task.matched_count} 已匹配 · {task.exception_count} 异常
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<ArrowRight className="w-4 h-4 text-muted-foreground" />
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</motion.div>
|
|
|
|
{/* Quick Actions */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 12 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.5, delay: 0.4, ease: [0.16, 1, 0.3, 1] as const }}
|
|
>
|
|
<h2 className="text-heading-3 text-foreground mb-4">快捷操作</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<Card
|
|
className="group cursor-pointer hover-lift"
|
|
onClick={() => router.push("/tasks/new")}
|
|
>
|
|
<CardContent className="p-5 flex items-start gap-4">
|
|
<div className="p-2.5 rounded-lg bg-muted group-hover:bg-primary/10 transition-colors">
|
|
<FileText className="w-5 h-5 text-muted-foreground group-hover:text-primary transition-colors" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-medium text-foreground mb-1 group-hover:text-primary transition-colors">
|
|
新建对账任务
|
|
</h3>
|
|
<p className="text-caption text-muted-foreground">
|
|
上传工资表、社保表进行对账
|
|
</p>
|
|
</div>
|
|
<ArrowRight className="w-4 h-4 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all self-center" />
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card
|
|
className="group cursor-pointer hover-lift"
|
|
onClick={() => router.push("/exceptions")}
|
|
>
|
|
<CardContent className="p-5 flex items-start gap-4">
|
|
<div className="p-2.5 rounded-lg bg-muted group-hover:bg-amber-500/10 transition-colors">
|
|
<AlertTriangle className="w-5 h-5 text-muted-foreground group-hover:text-amber-500 transition-colors" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-medium text-foreground mb-1 group-hover:text-amber-500 transition-colors">
|
|
查看异常
|
|
</h3>
|
|
<p className="text-caption text-muted-foreground">
|
|
处理待解决的异常记录
|
|
</p>
|
|
</div>
|
|
<ArrowRight className="w-4 h-4 text-muted-foreground group-hover:text-amber-500 group-hover:translate-x-1 transition-all self-center" />
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card
|
|
className="group cursor-pointer hover-lift"
|
|
onClick={() => router.push("/settings/rules")}
|
|
>
|
|
<CardContent className="p-5 flex items-start gap-4">
|
|
<div className="p-2.5 rounded-lg bg-muted group-hover:bg-violet-500/10 transition-colors">
|
|
<TrendingUp className="w-5 h-5 text-muted-foreground group-hover:text-violet-500 transition-colors" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-medium text-foreground mb-1 group-hover:text-violet-500 transition-colors">
|
|
对账规则
|
|
</h3>
|
|
<p className="text-caption text-muted-foreground">
|
|
配置对账规则和映射关系
|
|
</p>
|
|
</div>
|
|
<ArrowRight className="w-4 h-4 text-muted-foreground group-hover:text-violet-500 group-hover:translate-x-1 transition-all self-center" />
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</motion.div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
} |