增加审批组件

This commit is contained in:
dengqichen 2025-10-24 22:20:10 +08:00
parent ab89ebe994
commit ab4ea6e367
3 changed files with 609 additions and 8 deletions

View File

@ -0,0 +1,559 @@
import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { useForm } from "react-hook-form";
import { DataTablePagination } from '@/components/ui/pagination';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import {
Plus,
Edit,
Trash2,
Search,
Loader2,
FolderKanban,
CheckCircle2,
XCircle,
} from "lucide-react";
import DynamicIcon from '@/components/DynamicIcon';
import LucideIconSelect from '@/components/LucideIconSelect';
import {
getCategories,
createCategory,
updateCategory,
deleteCategory
} from '../../Category/service';
import type {
FormCategoryResponse,
FormCategoryRequest,
FormCategoryQuery
} from '../../Category/types';
import type { Page } from '@/types/base';
interface CategoryManageDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
}
const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
open,
onOpenChange,
onSuccess
}) => {
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const [data, setData] = useState<Page<FormCategoryResponse> | null>(null);
const [searchText, setSearchText] = useState('');
const [editMode, setEditMode] = useState(false);
const [editRecord, setEditRecord] = useState<FormCategoryResponse | null>(null);
const [iconSelectOpen, setIconSelectOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteRecord, setDeleteRecord] = useState<FormCategoryResponse | null>(null);
// 分页状态
const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const form = useForm<FormCategoryRequest>({
defaultValues: {
name: '',
code: '',
icon: '',
sort: 0,
description: '',
enabled: true,
}
});
// 加载分类列表
const loadCategories = async () => {
setLoading(true);
try {
const query: FormCategoryQuery = {
name: searchText || undefined,
pageNum,
pageSize,
};
const result = await getCategories(query);
setData(result || null);
} catch (error) {
console.error('加载分类失败:', error);
toast({
variant: "destructive",
title: "加载失败",
description: error instanceof Error ? error.message : '未知错误',
});
} finally {
setLoading(false);
}
};
useEffect(() => {
if (open) {
loadCategories();
}
}, [open, searchText, pageNum, pageSize]);
// 搜索
const handleSearch = () => {
setPageNum(0); // 搜索时重置到第一页
};
// 新建
const handleCreate = () => {
form.reset({
name: '',
code: '',
icon: '',
sort: 0,
description: '',
enabled: true,
});
setEditRecord(null);
setEditMode(true);
};
// 编辑
const handleEdit = (record: FormCategoryResponse) => {
setEditRecord(record);
form.reset({
name: record.name,
code: record.code,
icon: record.icon || '',
sort: record.sort,
description: record.description || '',
enabled: record.enabled,
});
setEditMode(true);
};
// 打开删除确认对话框
const handleDeleteClick = (record: FormCategoryResponse) => {
setDeleteRecord(record);
setDeleteDialogOpen(true);
};
// 确认删除
const confirmDelete = async () => {
if (!deleteRecord) return;
try {
await deleteCategory(deleteRecord.id);
toast({
title: "删除成功",
description: `分类 "${deleteRecord.name}" 已删除`,
});
loadCategories();
onSuccess?.();
setDeleteDialogOpen(false);
setDeleteRecord(null);
} catch (error) {
console.error('删除失败:', error);
toast({
variant: "destructive",
title: "删除失败",
description: error instanceof Error ? error.message : '未知错误',
});
}
};
// 保存
const handleSave = async (values: FormCategoryRequest) => {
try {
if (editRecord) {
await updateCategory(editRecord.id, values);
toast({
title: "更新成功",
description: `分类 "${values.name}" 已更新`,
});
} else {
await createCategory(values);
toast({
title: "创建成功",
description: `分类 "${values.name}" 已创建`,
});
}
setEditMode(false);
loadCategories();
onSuccess?.();
} catch (error) {
console.error('保存失败:', error);
toast({
variant: "destructive",
title: "保存失败",
description: error instanceof Error ? error.message : '未知错误',
});
}
};
// 取消编辑
const handleCancel = () => {
setEditMode(false);
setEditRecord(null);
form.reset();
};
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FolderKanban className="h-5 w-5" />
</DialogTitle>
</DialogHeader>
{!editMode ? (
<div className="space-y-4">
{/* 搜索栏 */}
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="搜索分类名称..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className="pl-10"
/>
</div>
<Button onClick={handleCreate}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
{/* 表格 */}
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[160px]"></TableHead>
<TableHead className="w-[140px]"></TableHead>
<TableHead className="w-[60px]"></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="min-w-[150px]"></TableHead>
<TableHead className="w-[100px] sticky right-0 bg-background shadow-[-2px_0_4px_rgba(0,0,0,0.05)]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-8">
<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) => (
<TableRow key={record.id}>
<TableCell className="font-medium">
{record.name}
</TableCell>
<TableCell>
<code className="text-xs bg-muted px-2 py-0.5 rounded whitespace-nowrap">
{record.code}
</code>
</TableCell>
<TableCell>
{record.icon ? (
<div className="flex items-center justify-center">
<DynamicIcon name={record.icon} className="h-5 w-5" />
</div>
) : (
<span className="text-muted-foreground text-sm">-</span>
)}
</TableCell>
<TableCell className="text-center">{record.sort}</TableCell>
<TableCell>
{record.enabled ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<XCircle className="h-4 w-4 text-gray-400" />
)}
</TableCell>
<TableCell className="max-w-[200px] truncate" title={record.description}>
{record.description || '-'}
</TableCell>
<TableCell className="sticky right-0 bg-background shadow-[-2px_0_4px_rgba(0,0,0,0.05)]">
<div className="flex items-center justify-end gap-1">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => handleEdit(record)}
>
<Edit className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => handleDeleteClick(record)}
>
<Trash2 className="h-3.5 w-3.5 text-destructive" />
</Button>
</div>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={7} className="text-center py-12 text-muted-foreground">
<div className="flex flex-col items-center gap-3">
<FolderKanban className="h-12 w-12 opacity-20" />
<p className="text-sm"></p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* 分页 */}
{data && data.totalElements > 0 && (
<div className="mt-4">
<DataTablePagination
pageIndex={pageNum + 1}
pageSize={pageSize}
pageCount={Math.ceil((data.totalElements || 0) / pageSize)}
onPageChange={(page) => setPageNum(page - 1)}
/>
</div>
)}
</div>
) : (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSave)} className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="name"
rules={{ required: '请输入分类名称' }}
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="输入分类名称" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="code"
rules={{ required: '请输入分类代码' }}
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input placeholder="输入分类代码" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="icon"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<div className="flex items-center gap-2">
<Input
placeholder="点击选择图标"
value={field.value}
readOnly
onClick={() => setIconSelectOpen(true)}
className="cursor-pointer"
/>
{field.value && (
<div className="flex items-center justify-center w-10 h-10 border rounded-md">
<DynamicIcon name={field.value} className="h-5 w-5" />
</div>
)}
</div>
</FormControl>
<FormMessage />
<LucideIconSelect
open={iconSelectOpen}
onOpenChange={setIconSelectOpen}
value={field.value}
onChange={field.onChange}
/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="sort"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
type="number"
placeholder="输入排序值"
{...field}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Textarea
placeholder="输入分类描述"
className="resize-none"
rows={3}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base"></FormLabel>
<div className="text-sm text-muted-foreground">
</div>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<div className="flex items-center justify-end gap-2 pt-4 border-t">
<Button type="button" variant="outline" onClick={handleCancel}>
</Button>
<Button type="submit">
</Button>
</div>
</form>
</Form>
)}
</DialogContent>
</Dialog>
{/* 删除确认对话框 */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="space-y-3">
<p></p>
{deleteRecord && (
<div className="rounded-md border p-3 space-y-2 bg-muted/50">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">:</span>
<span className="font-medium">{deleteRecord.name}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">:</span>
<code className="text-xs bg-background px-2 py-1 rounded">
{deleteRecord.code}
</code>
</div>
{deleteRecord.icon && (
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">:</span>
<DynamicIcon name={deleteRecord.icon} className="h-4 w-4" />
</div>
)}
{deleteRecord.description && (
<div className="flex items-start gap-2">
<span className="text-sm font-medium text-muted-foreground">:</span>
<span className="text-sm">{deleteRecord.description}</span>
</div>
)}
</div>
)}
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
};
export default CategoryManageDialog;

View File

@ -12,7 +12,7 @@ import { FormRenderer } from '@/components/FormDesigner';
import {
Loader2, Plus, Search, Eye, Edit, FileText, Ban, Trash2,
Database, MoreHorizontal, CheckCircle2, XCircle, Clock, AlertCircle,
Folder, Activity, Settings
Folder, Activity, Settings, FolderKanban
} from 'lucide-react';
import {
DropdownMenu,
@ -30,6 +30,7 @@ import type { Page } from '@/types/base';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import CreateModal from './components/CreateModal';
import EditBasicInfoModal from './components/EditBasicInfoModal';
import CategoryManageDialog from './components/CategoryManageDialog';
/**
*
@ -47,6 +48,9 @@ const FormDefinitionList: React.FC = () => {
status: undefined as FormDefinitionStatus | undefined,
});
// 分类管理弹窗
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
// 创建表单弹窗
const [createModalVisible, setCreateModalVisible] = useState(false);
@ -297,11 +301,17 @@ const FormDefinitionList: React.FC = () => {
</CardDescription>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => setCategoryDialogOpen(true)}>
<FolderKanban className="h-4 w-4 mr-2" />
</Button>
<Button onClick={handleCreate} size="default">
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
</CardHeader>
<Separator />
<CardContent className="pt-6">
@ -606,6 +616,13 @@ const FormDefinitionList: React.FC = () => {
loadData();
}}
/>
{/* 分类管理弹窗 */}
<CategoryManageDialog
open={categoryDialogOpen}
onOpenChange={setCategoryDialogOpen}
onSuccess={loadCategories}
/>
</div>
);
};

View File

@ -38,6 +38,8 @@ import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/components/ui/use-toast";
import { useForm } from "react-hook-form";
import { DataTablePagination } from '@/components/ui/pagination';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import {
Plus,
Edit,
@ -87,6 +89,10 @@ const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteRecord, setDeleteRecord] = useState<WorkflowCategoryResponse | null>(null);
// 分页状态
const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const form = useForm<WorkflowCategoryRequest>({
defaultValues: {
name: '',
@ -104,8 +110,8 @@ const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
setLoading(true);
try {
const query: WorkflowCategoryQuery = {
pageNum: 0,
pageSize: 100,
pageNum,
pageSize,
};
if (searchText) {
query.name = searchText;
@ -128,7 +134,14 @@ const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
if (open) {
loadCategories();
}
}, [open, searchText]);
}, [open, searchText, pageNum, pageSize]);
// 搜索时重置页码
useEffect(() => {
if (searchText !== '') {
setPageNum(0);
}
}, [searchText]);
// 触发方式选项
const triggerOptions = [
@ -382,6 +395,18 @@ const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
</TableBody>
</Table>
</div>
{/* 分页 */}
{data && data.totalElements > 0 && (
<div className="mt-4">
<DataTablePagination
pageIndex={pageNum + 1}
pageSize={pageSize}
pageCount={Math.ceil((data.totalElements || 0) / pageSize)}
onPageChange={(page) => setPageNum(page - 1)}
/>
</div>
)}
</div>
) : (
/* 编辑表单 */