feebbb10ac
- 使用公共Pagination组件统一各页面的分页逻辑 - 优化exceptions页面异常列表展示 - 简化settings/rules页面规则管理 - 后端tasks.py增加分页参数支持
653 lines
27 KiB
TypeScript
653 lines
27 KiB
TypeScript
"use client";
|
|
|
|
import { Suspense, useState, useEffect } from "react";
|
|
import { useSearchParams } from "next/navigation";
|
|
import { motion } from "motion/react";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { AlertTriangle,
|
|
CheckCircle,
|
|
Clock,
|
|
Eye,
|
|
Search,
|
|
XCircle,
|
|
} from "lucide-react";
|
|
import { api } from "@/lib/api/client";
|
|
import { Pagination } from "@/components/ui/pagination";
|
|
|
|
interface ExceptionItem {
|
|
id: number;
|
|
task_id: number;
|
|
exception_type: string;
|
|
severity: "low" | "medium" | "high" | "critical";
|
|
status: "pending" | "processing" | "resolved" | "ignored";
|
|
employee_id: string;
|
|
employee_name: string;
|
|
description: string;
|
|
detail: string | null;
|
|
salary_amount: number | null;
|
|
social_security_amount: number | null;
|
|
tax_amount: number | null;
|
|
bank_amount: number | null;
|
|
difference_amount: number | null;
|
|
resolution: string | null;
|
|
handler: string | null;
|
|
note: string | null;
|
|
suggested_action: string | null;
|
|
created_at: string;
|
|
updated_at: string | null;
|
|
}
|
|
|
|
interface Summary {
|
|
by_status: Record<string, number>;
|
|
by_severity: Record<string, number>;
|
|
by_type: Record<string, number>;
|
|
}
|
|
|
|
const severityConfig = {
|
|
low: { label: "低", color: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300", icon: "bg-blue-500" },
|
|
medium: { label: "中", color: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300", icon: "bg-yellow-500" },
|
|
high: { label: "高", color: "bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300", icon: "bg-orange-500" },
|
|
critical: { label: "严重", color: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300", icon: "bg-red-500" },
|
|
};
|
|
|
|
const statusConfig = {
|
|
pending: { label: "待处理", color: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300" },
|
|
processing: { label: "处理中", color: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300" },
|
|
confirmed: { label: "已确认", color: "bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300" },
|
|
resolved: { label: "已解决", color: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300" },
|
|
ignored: { label: "已忽略", color: "bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-500" },
|
|
};
|
|
|
|
const typeLabels: Record<string, string> = {
|
|
amount_mismatch: "金额不一致",
|
|
missing_record: "记录缺失",
|
|
duplicate_record: "重复记录",
|
|
format_error: "格式错误",
|
|
logic_error: "逻辑错误",
|
|
rule_violation: "规则违规",
|
|
threshold_exceeded: "阈值超限",
|
|
missing_employee: "员工缺失",
|
|
extra_employee: "多余员工",
|
|
amount_difference: "金额差异",
|
|
zero_amount: "金额为零",
|
|
negative_amount: "负数金额",
|
|
unusual_amount: "金额异常",
|
|
duplicate_employee: "重复员工",
|
|
bank_mismatch: "银行不匹配",
|
|
bank_missing: "银行记录缺失",
|
|
bank_extra: "银行多余记录",
|
|
AMOUNT_MISMATCH: "金额不一致",
|
|
MISSING_RECORD: "记录缺失",
|
|
DUPLICATE_RECORD: "重复记录",
|
|
FORMAT_ERROR: "格式错误",
|
|
LOGIC_ERROR: "逻辑错误",
|
|
RULE_VIOLATION: "规则违规",
|
|
THRESHOLD_EXCEEDED: "阈值超限",
|
|
MISSING_EMPLOYEE: "员工缺失",
|
|
EXTRA_EMPLOYEE: "多余员工",
|
|
AMOUNT_DIFFERENCE: "金额差异",
|
|
ZERO_AMOUNT: "金额为零",
|
|
NEGATIVE_AMOUNT: "负数金额",
|
|
UNUSUAL_AMOUNT: "金额异常",
|
|
DUPLICATE_EMPLOYEE: "重复员工",
|
|
BANK_MISMATCH: "银行不匹配",
|
|
BANK_MISSING: "银行记录缺失",
|
|
BANK_EXTRA: "银行多余记录",
|
|
};
|
|
|
|
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 } },
|
|
};
|
|
|
|
function ExceptionsContent() {
|
|
const searchParams = useSearchParams();
|
|
|
|
const [exceptions, setExceptions] = useState<ExceptionItem[]>([]);
|
|
const [summary, setSummary] = useState<Summary | null>(null);
|
|
const [total, setTotal] = useState(0);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const [taskId, setTaskId] = useState(searchParams.get("task_id") || "");
|
|
const [status, setStatus] = useState(searchParams.get("status") || "");
|
|
const [severity, setSeverity] = useState(searchParams.get("severity") || "");
|
|
const [exceptionType, setExceptionType] = useState(searchParams.get("type") || "");
|
|
const [page, setPage] = useState(Number(searchParams.get("page")) || 1);
|
|
const [pageSize, setPageSize] = useState(10);
|
|
|
|
const [selectedException, setSelectedException] = useState<ExceptionItem | null>(null);
|
|
const [detailOpen, setDetailOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetchExceptions();
|
|
fetchSummary();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [taskId, status, severity, exceptionType]);
|
|
|
|
function onPageChange(newPage: number, newPageSize: number) {
|
|
setPage(newPage);
|
|
if (newPageSize !== pageSize) {
|
|
setPageSize(newPageSize);
|
|
}
|
|
fetchExceptions();
|
|
fetchSummary();
|
|
}
|
|
|
|
async function fetchExceptions() {
|
|
setLoading(true);
|
|
try {
|
|
const res = await api.get<{ items: ExceptionItem[]; total: number }>("/api/exceptions/", {
|
|
params: {
|
|
task_id: taskId || undefined,
|
|
status: status || undefined,
|
|
severity: severity || undefined,
|
|
exception_type: exceptionType || undefined,
|
|
page,
|
|
page_size: pageSize,
|
|
},
|
|
});
|
|
setExceptions(res.data.items || []);
|
|
setTotal(res.data.total || 0);
|
|
} catch (error) {
|
|
console.error("加载异常列表失败", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
async function fetchSummary() {
|
|
try {
|
|
const res = await api.get<Summary>("/api/exceptions/summary", {
|
|
params: { task_id: taskId || undefined },
|
|
});
|
|
setSummary(res.data);
|
|
} catch (error) {
|
|
console.error("加载汇总失败", error);
|
|
}
|
|
}
|
|
|
|
function handleSearch() {
|
|
setPage(1);
|
|
fetchExceptions();
|
|
fetchSummary();
|
|
}
|
|
|
|
function viewDetail(exception: ExceptionItem) {
|
|
setSelectedException(exception);
|
|
setDetailOpen(true);
|
|
}
|
|
|
|
async function updateStatus(id: number, newStatus: string) {
|
|
try {
|
|
await api.patch(`/api/exceptions/${id}`, { status: newStatus });
|
|
fetchExceptions();
|
|
fetchSummary();
|
|
} catch (error) {
|
|
console.error("更新状态失败", error);
|
|
}
|
|
}
|
|
|
|
const totalPages = Math.ceil(total / pageSize);
|
|
|
|
return (
|
|
<div className="min-h-full p-6 lg:p-8 max-w-7xl mx-auto space-y-6">
|
|
{/* Header */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
|
|
className="flex items-center justify-between"
|
|
>
|
|
<div>
|
|
<h1 className="text-heading-1 text-foreground">异常处理</h1>
|
|
<p className="text-muted-foreground mt-1">管理和处理对账过程中发现的异常</p>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* Summary Cards */}
|
|
{summary && (
|
|
<motion.div
|
|
variants={container}
|
|
initial="hidden"
|
|
animate="show"
|
|
className="grid grid-cols-2 lg:grid-cols-4 gap-4"
|
|
>
|
|
<motion.div variants={item}>
|
|
<Card className="hover-lift">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-caption text-muted-foreground uppercase tracking-wide">待处理</p>
|
|
<p className="text-2xl font-semibold mt-1">{summary.by_status?.pending || 0}</p>
|
|
</div>
|
|
<div className="p-2.5 rounded-lg bg-gray-100 dark:bg-gray-800">
|
|
<Clock className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
|
|
<motion.div variants={item}>
|
|
<Card className="hover-lift">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-caption text-muted-foreground uppercase tracking-wide">处理中</p>
|
|
<p className="text-2xl font-semibold mt-1 text-amber-600">{summary.by_status?.processing || 0}</p>
|
|
</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>
|
|
</Card>
|
|
</motion.div>
|
|
|
|
<motion.div variants={item}>
|
|
<Card className="hover-lift">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-caption text-muted-foreground uppercase tracking-wide">已解决</p>
|
|
<p className="text-2xl font-semibold mt-1 text-emerald-600">{summary.by_status?.resolved || 0}</p>
|
|
</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>
|
|
</Card>
|
|
</motion.div>
|
|
|
|
<motion.div variants={item}>
|
|
<Card className="hover-lift">
|
|
<CardContent className="p-5">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-caption text-muted-foreground uppercase tracking-wide">严重异常</p>
|
|
<p className="text-2xl font-semibold mt-1 text-red-600">{summary.by_severity?.critical || 0}</p>
|
|
</div>
|
|
<div className="p-2.5 rounded-lg bg-red-50 dark:bg-red-950/30">
|
|
<XCircle className="w-5 h-5 text-red-600" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Filter Card */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.2, ease: [0.16, 1, 0.3, 1] }}
|
|
>
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex flex-wrap items-end gap-3">
|
|
<div className="flex-1 min-w-[180px]">
|
|
<label className="text-caption text-muted-foreground mb-1.5 block">任务ID</label>
|
|
<div className="relative">
|
|
<Input
|
|
placeholder="输入任务ID"
|
|
value={taskId}
|
|
onChange={(e) => setTaskId(e.target.value)}
|
|
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
|
className="h-9"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="w-[150px]">
|
|
<label className="text-caption text-muted-foreground mb-1.5 block">状态</label>
|
|
<Select value={status} onValueChange={setStatus}>
|
|
<SelectTrigger className="h-9">
|
|
<SelectValue placeholder="全部状态" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="">全部状态</SelectItem>
|
|
<SelectItem value="pending">待处理</SelectItem>
|
|
<SelectItem value="processing">处理中</SelectItem>
|
|
<SelectItem value="resolved">已解决</SelectItem>
|
|
<SelectItem value="ignored">已忽略</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="w-[150px]">
|
|
<label className="text-caption text-muted-foreground mb-1.5 block">严重程度</label>
|
|
<Select value={severity} onValueChange={setSeverity}>
|
|
<SelectTrigger className="h-9">
|
|
<SelectValue placeholder="全部程度" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="">全部程度</SelectItem>
|
|
<SelectItem value="low">低</SelectItem>
|
|
<SelectItem value="medium">中</SelectItem>
|
|
<SelectItem value="high">高</SelectItem>
|
|
<SelectItem value="critical">严重</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="w-[180px]">
|
|
<label className="text-caption text-muted-foreground mb-1.5 block">异常类型</label>
|
|
<Select value={exceptionType} onValueChange={setExceptionType}>
|
|
<SelectTrigger className="h-9">
|
|
<SelectValue placeholder="全部类型" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="">全部类型</SelectItem>
|
|
<SelectItem value="amount_mismatch">金额不一致</SelectItem>
|
|
<SelectItem value="missing_record">记录缺失</SelectItem>
|
|
<SelectItem value="duplicate_record">重复记录</SelectItem>
|
|
<SelectItem value="format_error">格式错误</SelectItem>
|
|
<SelectItem value="logic_error">逻辑错误</SelectItem>
|
|
<SelectItem value="missing_employee">员工缺失</SelectItem>
|
|
<SelectItem value="duplicate_employee">重复员工</SelectItem>
|
|
<SelectItem value="zero_amount">金额为零</SelectItem>
|
|
<SelectItem value="unusual_amount">金额异常</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<Button onClick={handleSearch} className="h-9 btn-press">
|
|
<Search className="w-4 h-4 mr-2" />
|
|
搜索
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
|
|
{/* Exceptions Table */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.4, delay: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
|
>
|
|
<Card>
|
|
<CardHeader className="pb-4">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="text-lg font-medium">异常列表</CardTitle>
|
|
<Badge variant="secondary">{total} 条记录</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
{total > 0 && (
|
|
<Pagination
|
|
page={page}
|
|
pageSize={pageSize}
|
|
total={total}
|
|
onChange={onPageChange}
|
|
/>
|
|
)}
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="hover:bg-transparent">
|
|
<TableHead className="w-[100px]">状态</TableHead>
|
|
<TableHead>员工</TableHead>
|
|
<TableHead>类型</TableHead>
|
|
<TableHead>严重程度</TableHead>
|
|
<TableHead>描述</TableHead>
|
|
<TableHead className="text-right">差异金额</TableHead>
|
|
<TableHead className="w-[100px]">操作</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{loading ? (
|
|
<TableRow>
|
|
<TableCell colSpan={7} className="text-center py-12">
|
|
<div className="flex items-center justify-center gap-2 text-muted-foreground">
|
|
<svg className="animate-spin w-5 h-5" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
</svg>
|
|
加载中...
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : exceptions.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={7} className="text-center py-12">
|
|
<div className="w-12 h-12 rounded-full bg-muted mx-auto mb-3 flex items-center justify-center">
|
|
<CheckCircle className="w-6 h-6 text-muted-foreground" />
|
|
</div>
|
|
<p className="text-foreground font-medium">暂无异常记录</p>
|
|
<p className="text-caption text-muted-foreground mt-1">所有记录都匹配成功</p>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
exceptions.map((exception) => (
|
|
<TableRow key={exception.id} className="group">
|
|
<TableCell>
|
|
<Badge className={statusConfig[exception.status]?.color || "bg-gray-100 text-gray-700"}>
|
|
{statusConfig[exception.status]?.label || exception.status}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div>
|
|
<div className="font-medium">{exception.employee_name}</div>
|
|
<div className="text-xs text-muted-foreground">{exception.employee_id}</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant="outline" className="font-normal">
|
|
{typeLabels[exception.exception_type] || typeLabels[exception.exception_type.toLowerCase()] || typeLabels[exception.exception_type.toUpperCase()] || exception.exception_type}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center gap-2">
|
|
<div className={`w-2 h-2 rounded-full ${severityConfig[exception.severity]?.icon || "bg-gray-500"}`} />
|
|
<Badge className={severityConfig[exception.severity]?.color || "bg-gray-100 text-gray-700"}>
|
|
{severityConfig[exception.severity]?.label || exception.severity}
|
|
</Badge>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="max-w-[200px]">
|
|
<span className="truncate block">{exception.description}</span>
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
{exception.difference_amount != null && (
|
|
<span className={exception.difference_amount > 0 ? "text-red-600" : "text-emerald-600"}>
|
|
{exception.difference_amount > 0 ? "+" : ""}
|
|
{exception.difference_amount.toFixed(2)}
|
|
</span>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<Button variant="ghost" size="sm" onClick={() => viewDetail(exception)} className="h-8 w-8 p-0">
|
|
<Eye className="h-4 w-4" />
|
|
</Button>
|
|
{exception.status === "pending" && (
|
|
<Button variant="ghost" size="sm" onClick={() => updateStatus(exception.id, "processing")} className="h-8 px-2">
|
|
处理
|
|
</Button>
|
|
)}
|
|
{exception.status === "processing" && (
|
|
<Button variant="ghost" size="sm" onClick={() => updateStatus(exception.id, "resolved")} className="h-8 px-2">
|
|
完成
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
|
|
{/* Detail Dialog */}
|
|
<Dialog open={detailOpen} onOpenChange={setDetailOpen}>
|
|
<DialogContent className="max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>异常详情</DialogTitle>
|
|
</DialogHeader>
|
|
{selectedException && (
|
|
<div className="space-y-6">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<p className="text-caption text-muted-foreground">员工姓名</p>
|
|
<p className="font-medium">{selectedException.employee_name}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-caption text-muted-foreground">员工ID</p>
|
|
<p className="font-medium">{selectedException.employee_id}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-caption text-muted-foreground">异常类型</p>
|
|
<Badge variant="outline">{typeLabels[selectedException.exception_type] || typeLabels[selectedException.exception_type.toLowerCase()] || typeLabels[selectedException.exception_type.toUpperCase()] || selectedException.exception_type}</Badge>
|
|
</div>
|
|
<div>
|
|
<p className="text-caption text-muted-foreground">严重程度</p>
|
|
<div className="flex items-center gap-2">
|
|
<div className={`w-2 h-2 rounded-full ${severityConfig[selectedException.severity]?.icon || "bg-gray-500"}`} />
|
|
<Badge className={severityConfig[selectedException.severity]?.color || "bg-gray-100 text-gray-700"}>
|
|
{severityConfig[selectedException.severity]?.label || selectedException.severity}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-t pt-4">
|
|
<h4 className="font-medium mb-3">金额详情</h4>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
{selectedException.salary_amount != null && (
|
|
<div className="p-3 rounded-lg bg-muted/50">
|
|
<p className="text-caption text-muted-foreground">工资</p>
|
|
<p className="font-mono font-medium mt-1">{selectedException.salary_amount.toFixed(2)}</p>
|
|
</div>
|
|
)}
|
|
{selectedException.social_security_amount != null && (
|
|
<div className="p-3 rounded-lg bg-muted/50">
|
|
<p className="text-caption text-muted-foreground">社保</p>
|
|
<p className="font-mono font-medium mt-1">{selectedException.social_security_amount.toFixed(2)}</p>
|
|
</div>
|
|
)}
|
|
{selectedException.tax_amount != null && (
|
|
<div className="p-3 rounded-lg bg-muted/50">
|
|
<p className="text-caption text-muted-foreground">个税</p>
|
|
<p className="font-mono font-medium mt-1">{selectedException.tax_amount.toFixed(2)}</p>
|
|
</div>
|
|
)}
|
|
{selectedException.bank_amount != null && (
|
|
<div className="p-3 rounded-lg bg-muted/50">
|
|
<p className="text-caption text-muted-foreground">银行</p>
|
|
<p className="font-mono font-medium mt-1">{selectedException.bank_amount.toFixed(2)}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{selectedException.difference_amount != null && (
|
|
<div className="mt-4 p-4 rounded-lg bg-muted/50">
|
|
<p className="text-caption text-muted-foreground">差异金额</p>
|
|
<p className={`text-2xl font-mono font-semibold mt-1 ${selectedException.difference_amount > 0 ? "text-red-600" : "text-emerald-600"}`}>
|
|
{selectedException.difference_amount > 0 ? "+" : ""}
|
|
{selectedException.difference_amount.toFixed(2)}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="border-t pt-4">
|
|
<div className="space-y-3">
|
|
<div>
|
|
<p className="text-caption text-muted-foreground mb-1">描述</p>
|
|
<p>{selectedException.description}</p>
|
|
</div>
|
|
{selectedException.detail && (
|
|
<div>
|
|
<p className="text-caption text-muted-foreground mb-1">详细信息</p>
|
|
<pre className="text-xs bg-muted p-3 rounded-lg overflow-auto">
|
|
{(() => {
|
|
try {
|
|
const detail = typeof selectedException.detail === 'string'
|
|
? JSON.parse(selectedException.detail)
|
|
: selectedException.detail;
|
|
return JSON.stringify(detail, null, 2);
|
|
} catch {
|
|
return String(selectedException.detail);
|
|
}
|
|
})()}
|
|
</pre>
|
|
</div>
|
|
)}
|
|
{selectedException.suggested_action && (
|
|
<div>
|
|
<p className="text-caption text-muted-foreground mb-1">建议操作</p>
|
|
<p>{selectedException.suggested_action}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function LoadingFallback() {
|
|
return (
|
|
<div className="min-h-full p-6 lg:p-8 flex items-center justify-center">
|
|
<div className="flex items-center gap-2 text-muted-foreground">
|
|
<svg className="animate-spin w-5 h-5" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
</svg>
|
|
加载中...
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function ExceptionsPage() {
|
|
return (
|
|
<Suspense fallback={<LoadingFallback />}>
|
|
<ExceptionsContent />
|
|
</Suspense>
|
|
);
|
|
} |