deploy-ease-platform/frontend/src/pages/Workflow/Definition/index.tsx
2025-10-25 11:36:27 +08:00

577 lines
22 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 } 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, Plus, Search, Edit, Trash2, Play, CheckCircle2,
Clock, Activity, Workflow, Eye, Pencil, FolderKanban
} from 'lucide-react';
import { useToast } from '@/components/ui/use-toast';
import { getDefinitions, getWorkflowCategoryList, deleteDefinition, publishDefinition, startWorkflowInstance } from './service';
import type { WorkflowDefinition, WorkflowDefinitionQuery, WorkflowCategoryResponse, WorkflowDefinitionStatus } from './types';
import type { Page } from '@/types/base';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import EditModal from './components/EditModal';
import DeleteDialog from './components/DeleteDialog';
import DeployDialog from './components/DeployDialog';
import CategoryManageDialog from './components/CategoryManageDialog';
import StartWorkflowModal from './components/StartWorkflowModal';
import { getDefinitionById as getFormDefinitionById } from '@/pages/Form/Definition/service';
import type { FormDefinitionResponse } from '@/pages/Form/Definition/types';
/**
* 工作流定义列表页
*/
const WorkflowDefinitionList: React.FC = () => {
const navigate = useNavigate();
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const [data, setData] = useState<Page<WorkflowDefinition> | null>(null);
const [categories, setCategories] = useState<WorkflowCategoryResponse[]>([]);
const [editModalVisible, setEditModalVisible] = useState(false);
const [editRecord, setEditRecord] = useState<WorkflowDefinition>();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteRecord, setDeleteRecord] = useState<WorkflowDefinition | null>(null);
const [deployDialogOpen, setDeployDialogOpen] = useState(false);
const [deployRecord, setDeployRecord] = useState<WorkflowDefinition | null>(null);
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
const [startModalVisible, setStartModalVisible] = useState(false);
const [startRecord, setStartRecord] = useState<WorkflowDefinition | null>(null);
const [formDefinition, setFormDefinition] = useState<FormDefinitionResponse | null>(null);
const [query, setQuery] = useState<WorkflowDefinitionQuery>({
pageNum: DEFAULT_CURRENT - 1,
pageSize: DEFAULT_PAGE_SIZE,
name: '',
categoryId: undefined,
status: undefined
});
// 加载数据
const loadData = async () => {
setLoading(true);
try {
const result = await getDefinitions(query);
setData(result);
} catch (error) {
console.error('加载工作流定义失败:', error);
} finally {
setLoading(false);
}
};
// 加载分类
const loadCategories = async () => {
try {
const result = await getWorkflowCategoryList();
setCategories(result || []);
} catch (error) {
console.error('加载分类失败:', error);
}
};
useEffect(() => {
loadCategories();
}, []);
useEffect(() => {
loadData();
}, [query]);
// 搜索
const handleSearch = () => {
setQuery(prev => ({ ...prev, pageNum: 0 }));
};
// 重置
const handleReset = () => {
setQuery({
pageNum: 0,
pageSize: DEFAULT_PAGE_SIZE,
name: '',
categoryId: undefined,
status: undefined
});
};
// 新建
const handleCreate = () => {
setEditRecord(undefined);
setEditModalVisible(true);
};
// 编辑
const handleEdit = (record: WorkflowDefinition) => {
setEditRecord(record);
setEditModalVisible(true);
};
// 设计
const handleDesign = (record: WorkflowDefinition) => {
navigate(`/workflow/design/${record.id}`);
};
// 发布
const handleDeploy = (record: WorkflowDefinition) => {
setDeployRecord(record);
setDeployDialogOpen(true);
};
const confirmDeploy = async () => {
if (!deployRecord) return;
try {
await publishDefinition(deployRecord.id);
toast({
title: '发布成功',
description: `工作流 "${deployRecord.name}" 已发布`,
});
loadData();
setDeployDialogOpen(false);
setDeployRecord(null);
} catch (error) {
console.error('发布失败:', error);
toast({
title: '发布失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
}
};
// 删除
const handleDelete = (record: WorkflowDefinition) => {
setDeleteRecord(record);
setDeleteDialogOpen(true);
};
const confirmDelete = async () => {
if (!deleteRecord) return;
try {
await deleteDefinition(deleteRecord.id);
toast({
title: '删除成功',
description: `工作流 "${deleteRecord.name}" 已删除`,
});
loadData();
setDeleteDialogOpen(false);
setDeleteRecord(null);
} catch (error) {
console.error('删除失败:', error);
toast({
title: '删除失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
}
};
// 启动工作流
const handleStart = async (record: WorkflowDefinition) => {
// 检查是否有关联的启动表单
if (record.formDefinitionId) {
// 加载表单定义数据
try {
const formDef = await getFormDefinitionById(record.formDefinitionId);
setFormDefinition(formDef);
setStartRecord(record);
setStartModalVisible(true);
} catch (error) {
console.error('加载启动表单失败:', error);
toast({
title: '加载失败',
description: '无法加载启动表单,请稍后重试',
variant: 'destructive'
});
}
} else {
// 没有启动表单,直接启动(不传递表单数据)
try {
const result = await startWorkflowInstance({
processKey: record.key
});
console.log('🚀 工作流启动成功:', result);
toast({
title: '启动成功',
description: `工作流 "${record.name}" 已启动`,
});
loadData(); // 刷新列表
} catch (error) {
console.error('启动失败:', error);
toast({
title: '启动失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
}
}
};
// 处理启动工作流提交(带表单数据)
const handleStartWorkflowSubmit = async (workflow: WorkflowDefinition, formData: Record<string, any>) => {
try {
console.log('🚀 启动工作流,携带表单数据:', formData);
// 调用启动API传递表单数据
const result = await startWorkflowInstance({
processKey: workflow.key, // 流程定义key
formKey: formDefinition?.key, // 表单标识
formData: formData, // 表单数据
businessKey: undefined // 自动生成 businessKey
});
console.log('✅ 工作流实例已创建:', result);
toast({
title: '启动成功',
description: `工作流 "${workflow.name}" 已启动`,
});
loadData(); // 刷新列表
} catch (error) {
console.error('❌ 启动失败:', error);
toast({
title: '启动失败',
description: error instanceof Error ? error.message : '未知错误',
variant: 'destructive'
});
throw error; // 重新抛出让弹窗知道失败了
}
};
// 状态徽章
const getStatusBadge = (status: string) => {
const statusMap: Record<string, {
variant: 'default' | 'secondary' | 'destructive' | 'success' | 'outline';
text: string;
icon: React.ElementType
}> = {
DRAFT: { variant: 'outline', text: '草稿', icon: Clock },
PUBLISHED: { variant: 'success', text: '已发布', icon: CheckCircle2 },
DISABLED: { variant: 'secondary', text: '已停用', icon: Clock },
};
const statusInfo = statusMap[status] || { variant: 'outline', text: status, icon: Clock };
const Icon = statusInfo.icon;
return (
<Badge variant={statusInfo.variant} className="inline-flex items-center gap-1">
<Icon className="h-3 w-3" />
{statusInfo.text}
</Badge>
);
};
// 统计数据
const stats = useMemo(() => {
const total = data?.totalElements || 0;
const draftCount = data?.content?.filter(d => d.status === 'DRAFT').length || 0;
const publishedCount = data?.content?.filter(d => d.status === 'PUBLISHED').length || 0;
return { total, draftCount, publishedCount };
}, [data]);
const pageCount = data?.totalElements ? Math.ceil(data.totalElements / (query.pageSize || DEFAULT_PAGE_SIZE)) : 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-3 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.publishedCount}</div>
<p className="text-xs text-muted-foreground mt-1">使</p>
</CardContent>
</Card>
</div>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle></CardTitle>
<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}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</CardHeader>
<CardContent>
{/* 搜索栏 */}
<div className="flex flex-wrap items-center gap-4 mb-4">
<div className="flex-1 max-w-md">
<Input
placeholder="搜索工作流名称"
value={query.name}
onChange={(e) => setQuery(prev => ({ ...prev, name: 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()}>
{cat.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={query.status || undefined}
onValueChange={(value) => setQuery(prev => ({ ...prev, status: value as WorkflowDefinitionStatus | undefined }))}
>
<SelectTrigger className="w-[140px] h-9">
<SelectValue placeholder="全部状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="DRAFT">稿</SelectItem>
<SelectItem value="PUBLISHED"></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>
</div>
{/* 表格 */}
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[200px]"></TableHead>
<TableHead className="w-[150px]"></TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHead className="w-[80px]"></TableHead>
<TableHead className="w-[120px]"></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={7} 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 = categories.find(c => c.id === record.categoryId);
const isDraft = record.status === 'DRAFT';
return (
<TableRow key={record.id} className="hover:bg-muted/50">
<TableCell className="font-medium">{record.name}</TableCell>
<TableCell>
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">
{record.key}
</code>
</TableCell>
<TableCell>
{categoryInfo ? (
<Badge variant="outline">{categoryInfo.name}</Badge>
) : (
<Badge variant="outline"></Badge>
)}
</TableCell>
<TableCell>
<span className="font-mono text-sm">{record.flowVersion || 1}</span>
</TableCell>
<TableCell>{getStatusBadge(record.status || 'DRAFT')}</TableCell>
<TableCell>
<span className="text-sm line-clamp-1">{record.description || '-'}</span>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
{isDraft ? (
<>
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(record)}
title="编辑"
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDesign(record)}
title="设计"
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeploy(record)}
title="发布"
className="text-green-600 hover:text-green-700 hover:bg-green-50"
>
<CheckCircle2 className="h-4 w-4" />
</Button>
</>
) : (
<>
<Button
variant="ghost"
size="sm"
onClick={() => handleStart(record)}
title="启动"
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50"
>
<Play className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDesign(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={7} className="h-24 text-center">
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<Workflow 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 || 0) + 1}
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
pageCount={pageCount}
onPageChange={(page) => setQuery(prev => ({
...prev,
pageNum: page - 1
}))}
/>
)}
</CardContent>
</Card>
{/* 编辑弹窗 */}
<EditModal
visible={editModalVisible}
onClose={() => {
setEditModalVisible(false);
setEditRecord(undefined);
}}
onSuccess={loadData}
record={editRecord}
/>
{/* 发布确认对话框 */}
<DeployDialog
open={deployDialogOpen}
record={deployRecord}
onOpenChange={setDeployDialogOpen}
onConfirm={confirmDeploy}
/>
{/* 删除确认对话框 */}
<DeleteDialog
open={deleteDialogOpen}
record={deleteRecord}
onOpenChange={setDeleteDialogOpen}
onConfirm={confirmDelete}
/>
{/* 分类管理弹窗 */}
<CategoryManageDialog
open={categoryDialogOpen}
onOpenChange={setCategoryDialogOpen}
onSuccess={loadCategories}
/>
{/* 启动工作流弹窗 */}
<StartWorkflowModal
open={startModalVisible}
onClose={() => {
setStartModalVisible(false);
setStartRecord(null);
setFormDefinition(null);
}}
workflowDefinition={startRecord}
formDefinition={formDefinition}
onSubmit={handleStartWorkflowSubmit}
/>
</div>
);
};
export default WorkflowDefinitionList;