feat: UI优化和功能完善
- 添加公共分页组件,支持上边显示、分页大小10 - 侧边栏/Header按Liner风格优化,添加动画效果 - 异常处理列表支持分页和明细弹窗 - 任务结果页面支持已匹配列表分页 - 修复Dialog弹窗背景透明问题 - 新增dropdown-menu组件 - 添加parsed_file_record模型和回填脚本 - 前端枚举中文化
This commit is contained in:
@@ -2,8 +2,26 @@
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowLeft, Trash2, ToggleLeft, ToggleRight, Search } from "lucide-react";
|
||||
|
||||
import { motion } from "motion/react";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Trash2,
|
||||
ToggleLeft,
|
||||
ToggleRight,
|
||||
Search,
|
||||
FileText
|
||||
} from "lucide-react";
|
||||
import { Card, CardContent } 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 { api } from "@/lib/api/client";
|
||||
import { ENDPOINTS } from "@/lib/api/endpoints";
|
||||
import { useToast } from "@/lib/hooks/useToast";
|
||||
@@ -61,6 +79,19 @@ const RULE_TYPE_LABELS: Record<string, string> = {
|
||||
DEPARTMENT_MAPPING: "部门映射",
|
||||
};
|
||||
|
||||
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 } },
|
||||
};
|
||||
|
||||
export default function RulesPage() {
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
@@ -129,196 +160,275 @@ export default function RulesPage() {
|
||||
const inactiveCount = rules.filter((r) => r.status === "INACTIVE").length;
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-7xl mx-auto">
|
||||
<div className="min-h-full p-6 lg:p-8 max-w-7xl mx-auto">
|
||||
{/* 页面头部 */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<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 mb-8"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => router.push("/dashboard")}
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
</button>
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
规则管理
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
<h1 className="text-heading-1 text-foreground">规则管理</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
管理企业字段映射规则,实现自动识别
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{rules.length}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">总规则数</div>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="text-2xl font-bold text-green-600">{activeCount}</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">启用中</div>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="text-2xl font-bold text-gray-400">{inactiveCount}</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">已停用</div>
|
||||
</div>
|
||||
</div>
|
||||
<motion.div
|
||||
variants={container}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6"
|
||||
>
|
||||
<motion.div variants={item}>
|
||||
<Card className="hover-lift">
|
||||
<CardContent className="p-5">
|
||||
<p className="text-caption text-muted-foreground uppercase tracking-wide">总规则数</p>
|
||||
<p className="text-3xl font-semibold mt-1">{rules.length}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={item}>
|
||||
<Card className="hover-lift">
|
||||
<CardContent className="p-5">
|
||||
<p className="text-caption text-muted-foreground uppercase tracking-wide">启用中</p>
|
||||
<p className="text-3xl font-semibold mt-1 text-emerald-600">{activeCount}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={item}>
|
||||
<Card className="hover-lift">
|
||||
<CardContent className="p-5">
|
||||
<p className="text-caption text-muted-foreground uppercase tracking-wide">已停用</p>
|
||||
<p className="text-3xl font-semibold mt-1 text-muted-foreground">{inactiveCount}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* 筛选栏 */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索规则..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<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-[200px]">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="搜索规则..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="h-9 pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={typeFilter}
|
||||
onChange={(e) => setTypeFilter(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">全部类型</option>
|
||||
<option value="FIELD_MAPPING">字段映射</option>
|
||||
<option value="ACCOUNT_MAPPING">科目映射</option>
|
||||
<option value="DEPARTMENT_MAPPING">部门映射</option>
|
||||
</select>
|
||||
<div className="w-[150px]">
|
||||
<Select value={typeFilter} onValueChange={setTypeFilter}>
|
||||
<SelectTrigger className="h-9">
|
||||
<SelectValue placeholder="全部类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">全部类型</SelectItem>
|
||||
<SelectItem value="FIELD_MAPPING">字段映射</SelectItem>
|
||||
<SelectItem value="ACCOUNT_MAPPING">科目映射</SelectItem>
|
||||
<SelectItem value="DEPARTMENT_MAPPING">部门映射</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">全部状态</option>
|
||||
<option value="ACTIVE">启用</option>
|
||||
<option value="INACTIVE">停用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-[150px]">
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="h-9">
|
||||
<SelectValue placeholder="全部状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="">全部状态</SelectItem>
|
||||
<SelectItem value="ACTIVE">启用</SelectItem>
|
||||
<SelectItem value="INACTIVE">停用</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* 规则列表 */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-50 dark:bg-gray-900/50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-400">
|
||||
规则类型
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-400">
|
||||
匹配条件
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-medium text-gray-600 dark:text-gray-400">
|
||||
目标字段
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center font-medium text-gray-600 dark:text-gray-400">
|
||||
匹配次数
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center font-medium text-gray-600 dark:text-gray-400">
|
||||
状态
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center font-medium text-gray-600 dark:text-gray-400">
|
||||
最近使用
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right font-medium text-gray-600 dark:text-gray-400">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
{filteredRules.map((rule) => (
|
||||
<tr
|
||||
key={rule.id}
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-colors"
|
||||
>
|
||||
<td className="px-4 py-3">
|
||||
<span className="px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 rounded text-xs">
|
||||
{RULE_TYPE_LABELS[rule.rule_type] || rule.rule_type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 font-medium text-gray-900 dark:text-gray-100">
|
||||
{rule.match_condition.source_field || JSON.stringify(rule.match_condition)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-gray-700 dark:text-gray-300">
|
||||
{rule.target_value}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center text-gray-500 dark:text-gray-400">
|
||||
{rule.match_count}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<span
|
||||
className={`px-2 py-1 rounded text-xs ${
|
||||
rule.status === "ACTIVE"
|
||||
? "bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400"
|
||||
: "bg-gray-100 dark:bg-gray-700 text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{rule.status === "ACTIVE" ? "启用" : "停用"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center text-gray-500 dark:text-gray-400">
|
||||
{rule.last_used_at
|
||||
? new Date(rule.last_used_at).toLocaleDateString("zh-CN")
|
||||
: "-"}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button
|
||||
onClick={() => handleToggleStatus(rule.id, rule.status)}
|
||||
className={`p-1 rounded transition-colors ${
|
||||
rule.status === "ACTIVE"
|
||||
? "text-green-600 hover:bg-green-50 dark:hover:bg-green-900/30"
|
||||
: "text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
}`}
|
||||
title={rule.status === "ACTIVE" ? "停用" : "启用"}
|
||||
>
|
||||
{rule.status === "ACTIVE" ? (
|
||||
<ToggleRight className="w-5 h-5" />
|
||||
) : (
|
||||
<ToggleLeft className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteRule(rule.id)}
|
||||
className="p-1 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/30 rounded transition-colors"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{filteredRules.length === 0 && (
|
||||
<div className="px-4 py-12 text-center text-gray-500 dark:text-gray-400">
|
||||
暂无规则数据
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<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] }}
|
||||
className="mt-6"
|
||||
>
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="px-4 py-3 text-left font-medium text-muted-foreground uppercase tracking-wide text-xs">
|
||||
规则类型
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-medium text-muted-foreground uppercase tracking-wide text-xs">
|
||||
匹配条件
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left font-medium text-muted-foreground uppercase tracking-wide text-xs">
|
||||
目标字段
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center font-medium text-muted-foreground uppercase tracking-wide text-xs">
|
||||
匹配次数
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center font-medium text-muted-foreground uppercase tracking-wide text-xs">
|
||||
状态
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center font-medium text-muted-foreground uppercase tracking-wide text-xs">
|
||||
最近使用
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right font-medium text-muted-foreground uppercase tracking-wide text-xs">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border">
|
||||
{filteredRules.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-4 py-12 text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-muted mx-auto mb-3 flex items-center justify-center">
|
||||
<FileText 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>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredRules.map((rule, index) => (
|
||||
<motion.tr
|
||||
key={rule.id}
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.2, delay: index * 0.02 }}
|
||||
className="hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<td className="px-4 py-3">
|
||||
<Badge variant="outline">
|
||||
{RULE_TYPE_LABELS[rule.rule_type] || rule.rule_type}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 font-medium text-foreground">
|
||||
{rule.match_condition.source_field || JSON.stringify(rule.match_condition)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{rule.target_value}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center text-muted-foreground">
|
||||
{rule.match_count}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<Badge className={rule.status === "ACTIVE"
|
||||
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300"
|
||||
: "bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-500"
|
||||
}>
|
||||
{rule.status === "ACTIVE" ? "启用" : "停用"}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center text-muted-foreground text-caption">
|
||||
{rule.last_used_at
|
||||
? new Date(rule.last_used_at).toLocaleDateString("zh-CN")
|
||||
: "-"}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleToggleStatus(rule.id, rule.status)}
|
||||
className={`h-8 w-8 ${rule.status === "ACTIVE"
|
||||
? "text-emerald-600 hover:text-emerald-700 hover:bg-emerald-50 dark:hover:bg-emerald-900/30"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
title={rule.status === "ACTIVE" ? "停用" : "启用"}
|
||||
>
|
||||
{rule.status === "ACTIVE" ? (
|
||||
<ToggleRight className="w-5 h-5" />
|
||||
) : (
|
||||
<ToggleLeft className="w-5 h-5" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteRule(rule.id)}
|
||||
className="h-8 w-8 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</motion.tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* 说明 */}
|
||||
<div className="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||
<h3 className="font-medium text-blue-800 dark:text-blue-300 mb-2">
|
||||
规则说明
|
||||
</h3>
|
||||
<ul className="text-sm text-blue-700 dark:text-blue-400 space-y-1">
|
||||
<li>• 字段映射规则会在上传同类文件时自动应用</li>
|
||||
<li>• 精确匹配的规则(完全相同的字段名)会优先于模糊匹配</li>
|
||||
<li>• 停用规则不会在自动识别中应用,但不会删除</li>
|
||||
<li>• 删除规则后,下次上传相同格式文件需要重新确认映射</li>
|
||||
</ul>
|
||||
</div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, delay: 0.4, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="mt-6"
|
||||
>
|
||||
<Card className="border-blue-200 dark:border-blue-800 bg-blue-50/50 dark:bg-blue-950/20">
|
||||
<CardContent className="p-5">
|
||||
<h3 className="font-medium text-foreground mb-3">
|
||||
规则说明
|
||||
</h3>
|
||||
<ul className="text-body-small text-muted-foreground space-y-1.5">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-primary mt-0.5">•</span>
|
||||
字段映射规则会在上传同类文件时自动应用
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-primary mt-0.5">•</span>
|
||||
精确匹配的规则(完全相同的字段名)会优先于模糊匹配
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-primary mt-0.5">•</span>
|
||||
停用规则不会在自动识别中应用,但不会删除
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-primary mt-0.5">•</span>
|
||||
删除规则后,下次上传相同格式文件需要重新确认映射
|
||||
</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user