deploy-ease-platform/frontend/src/pages/Form/Data/index.tsx
2025-10-24 13:10:02 +08:00

409 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useMemo } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { DataTablePagination } from '@/components/ui/pagination';
import {
Loader2, Search, Eye, Trash2, Download, Folder,
Activity, Clock, CheckCircle2, FileCheck, Database
} from 'lucide-react';
import { getFormDataList, deleteFormData, exportFormData } from './service';
import { getEnabledCategories } from '../Category/service';
import type { FormDataResponse, FormDataStatus, FormDataBusinessType } from './types';
import type { FormCategoryResponse } from '../Category/types';
import type { Page } from '@/types/base';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import dayjs from 'dayjs';
/**
* 表单数据列表页
*/
const FormDataList: React.FC = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [loading, setLoading] = useState(false);
const [data, setData] = useState<Page<FormDataResponse> | null>(null);
const [categories, setCategories] = useState<FormCategoryResponse[]>([]);
const [query, setQuery] = useState({
pageNum: DEFAULT_CURRENT - 1,
pageSize: DEFAULT_PAGE_SIZE,
formDefinitionId: searchParams.get('formDefinitionId') ? Number(searchParams.get('formDefinitionId')) : undefined,
businessKey: '',
categoryId: undefined as number | undefined,
status: undefined as FormDataStatus | undefined,
businessType: undefined as FormDataBusinessType | undefined,
});
// 加载分类列表
const loadCategories = async () => {
try {
const result = await getEnabledCategories();
setCategories(result || []);
} catch (error) {
console.error('加载分类失败:', error);
}
};
// 加载数据
const loadData = async () => {
setLoading(true);
try {
const result = await getFormDataList(query);
setData(result);
} catch (error) {
console.error('加载表单数据失败:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadCategories();
}, []);
useEffect(() => {
loadData();
}, [query]);
// 搜索
const handleSearch = () => {
setQuery(prev => ({
...prev,
pageNum: 0,
}));
};
// 重置搜索
const handleReset = () => {
setQuery({
pageNum: 0,
pageSize: DEFAULT_PAGE_SIZE,
formDefinitionId: undefined,
businessKey: '',
categoryId: undefined,
status: undefined,
businessType: undefined,
});
};
// 根据分类 ID 获取分类信息
const getCategoryInfo = (categoryId?: number) => {
return categories.find(cat => cat.id === categoryId);
};
// 查看详情
const handleView = (record: FormDataResponse) => {
navigate(`/form/data/${record.id}`);
};
// 删除
const handleDelete = async (record: FormDataResponse) => {
if (!confirm('确定要删除该数据吗?')) return;
try {
await deleteFormData(record.id);
loadData();
} catch (error) {
console.error('删除数据失败:', error);
}
};
// 导出
const handleExport = async () => {
try {
await exportFormData(query);
} catch (error) {
console.error('导出数据失败:', error);
}
};
// 状态徽章
const getStatusBadge = (status: FormDataStatus) => {
const statusMap: Record<FormDataStatus, {
variant: 'default' | 'secondary' | 'destructive' | 'success' | 'outline';
text: string;
icon: React.ElementType
}> = {
DRAFT: { variant: 'outline', text: '草稿', icon: Clock },
SUBMITTED: { variant: 'success', text: '已提交', icon: CheckCircle2 },
COMPLETED: { variant: 'default', text: '已完成', icon: FileCheck },
};
const statusInfo = statusMap[status];
const Icon = statusInfo.icon;
return (
<Badge variant={statusInfo.variant} className="flex items-center gap-1">
<Icon className="h-3 w-3" />
{statusInfo.text}
</Badge>
);
};
// 业务类型徽章
const getBusinessTypeBadge = (type: FormDataBusinessType) => {
const typeMap: Record<FormDataBusinessType, { variant: 'default' | 'secondary' | 'outline'; text: string }> = {
STANDALONE: { variant: 'outline', text: '独立' },
WORKFLOW: { variant: 'default', text: '工作流' },
ORDER: { variant: 'secondary', text: '订单' },
};
const typeInfo = typeMap[type];
return <Badge variant={typeInfo.variant}>{typeInfo.text}</Badge>;
};
// 统计数据
const stats = useMemo(() => {
const total = data?.totalElements || 0;
const draftCount = data?.content?.filter(d => d.status === 'DRAFT').length || 0;
const submittedCount = data?.content?.filter(d => d.status === 'SUBMITTED').length || 0;
const completedCount = data?.content?.filter(d => d.status === 'COMPLETED').length || 0;
return { total, draftCount, submittedCount, completedCount };
}, [data]);
const pageCount = data?.totalElements ? Math.ceil(data.totalElements / query.pageSize) : 0;
return (
<div className="p-6">
<div className="mb-6">
<h1 className="text-3xl font-bold text-foreground"></h1>
<p className="text-muted-foreground mt-2">
</p>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<Card className="bg-gradient-to-br from-blue-500/10 to-blue-500/5 border-blue-500/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-blue-700"></CardTitle>
<Activity className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.total}</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-yellow-500/10 to-yellow-500/5 border-yellow-500/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-yellow-700">稿</CardTitle>
<Clock className="h-4 w-4 text-yellow-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.draftCount}</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-green-500/10 to-green-500/5 border-green-500/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-green-700"></CardTitle>
<CheckCircle2 className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.submittedCount}</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-purple-500/10 to-purple-500/5 border-purple-500/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-purple-700"></CardTitle>
<FileCheck className="h-4 w-4 text-purple-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.completedCount}</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
{/* 搜索栏 */}
<div className="flex flex-wrap items-center gap-4 mb-4">
<div className="flex-1 max-w-md">
<Input
placeholder="搜索业务标识"
value={query.businessKey}
onChange={(e) => setQuery(prev => ({ ...prev, businessKey: e.target.value }))}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className="h-9"
/>
</div>
<Select
value={query.categoryId?.toString() || undefined}
onValueChange={(value) => setQuery(prev => ({ ...prev, categoryId: Number(value) }))}
>
<SelectTrigger className="w-[160px] h-9">
<SelectValue placeholder="全部分类" />
</SelectTrigger>
<SelectContent>
{categories.map(cat => (
<SelectItem key={cat.id} value={cat.id.toString()}>
<div className="flex items-center gap-2">
{cat.icon && <Folder className="h-3.5 w-3.5" />}
{cat.name}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={query.businessType || undefined}
onValueChange={(value) => setQuery(prev => ({ ...prev, businessType: value as FormDataBusinessType }))}
>
<SelectTrigger className="w-[140px] h-9">
<SelectValue placeholder="全部类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="STANDALONE"></SelectItem>
<SelectItem value="WORKFLOW"></SelectItem>
<SelectItem value="ORDER"></SelectItem>
</SelectContent>
</Select>
<Select
value={query.status || undefined}
onValueChange={(value) => setQuery(prev => ({ ...prev, status: value as FormDataStatus }))}
>
<SelectTrigger className="w-[130px] h-9">
<SelectValue placeholder="全部状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="DRAFT">稿</SelectItem>
<SelectItem value="SUBMITTED"></SelectItem>
<SelectItem value="COMPLETED"></SelectItem>
</SelectContent>
</Select>
<Button onClick={handleSearch} className="h-9">
<Search className="h-4 w-4 mr-2" />
</Button>
<Button variant="outline" onClick={handleReset} className="h-9">
</Button>
<Button variant="outline" onClick={handleExport} className="h-9">
<Download className="h-4 w-4 mr-2" />
</Button>
</div>
{/* 表格 */}
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[180px]"></TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[150px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[180px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={8} className="h-24 text-center">
<div className="flex items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin mr-2" />
<span className="text-sm text-muted-foreground">...</span>
</div>
</TableCell>
</TableRow>
) : data?.content && data.content.length > 0 ? (
data.content.map((record) => {
const categoryInfo = getCategoryInfo(record.categoryId);
return (
<TableRow key={record.id} className="hover:bg-muted/50">
<TableCell>
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">
{record.formKey}
</code>
</TableCell>
<TableCell>
{categoryInfo ? (
<Badge variant="outline" className="flex items-center gap-1 w-fit">
{categoryInfo.icon && <Folder className="h-3 w-3" />}
{categoryInfo.name}
</Badge>
) : (
<Badge variant="outline"></Badge>
)}
</TableCell>
<TableCell>{getBusinessTypeBadge(record.businessType)}</TableCell>
<TableCell>
<span className="text-sm">{record.businessKey || '-'}</span>
</TableCell>
<TableCell>
<span className="text-sm">{record.submitter || '匿名'}</span>
</TableCell>
<TableCell>
<span className="text-sm">
{record.submitTime ? dayjs(record.submitTime).format('YYYY-MM-DD HH:mm:ss') : '-'}
</span>
</TableCell>
<TableCell>{getStatusBadge(record.status)}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleView(record)}
title="查看详情"
>
<Eye className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(record)}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
title="删除"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
);
})
) : (
<TableRow>
<TableCell colSpan={8} className="h-24 text-center">
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<Database className="w-16 h-16 mb-4 text-muted-foreground/50" />
<div className="text-lg font-semibold mb-2"></div>
<div className="text-sm"></div>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* 分页 */}
{pageCount > 1 && (
<DataTablePagination
pageIndex={query.pageNum + 1}
pageSize={query.pageSize}
pageCount={pageCount}
onPageChange={(page) => setQuery(prev => ({
...prev,
pageNum: page - 1
}))}
/>
)}
</CardContent>
</Card>
</div>
);
};
export default FormDataList;