deploy-ease-platform/frontend/src/pages/Workflow/Instance/index.tsx
2025-10-29 13:31:55 +08:00

323 lines
13 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 { 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, StopCircle, Activity, PlayCircle,
CheckCircle2, Clock, Workflow, XCircle, Pause
} from 'lucide-react';
import { getWorkflowInstances } from './service';
import type { WorkflowTemplateWithInstances } from './types';
import type { Page } from '@/types/base';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import HistoryModal from './components/HistoryModal';
import dayjs from 'dayjs';
/**
* 工作流实例列表页
*/
const WorkflowInstanceList: React.FC = () => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState<Page<WorkflowTemplateWithInstances> | null>(null);
const [historyVisible, setHistoryVisible] = useState(false);
const [selectedWorkflowDefinitionId, setSelectedWorkflowDefinitionId] = useState<number>();
const [query, setQuery] = useState({
pageNum: DEFAULT_CURRENT - 1,
pageSize: DEFAULT_PAGE_SIZE,
businessKey: '',
status: undefined as string | undefined,
});
// 加载数据
const loadData = async () => {
setLoading(true);
try {
const result = await getWorkflowInstances(query);
setData(result);
} catch (error) {
console.error('加载流程实例失败:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
}, [query]);
// 搜索
const handleSearch = () => {
setQuery(prev => ({
...prev,
pageNum: 0,
}));
};
// 重置搜索
const handleReset = () => {
setQuery({
pageNum: 0,
pageSize: DEFAULT_PAGE_SIZE,
businessKey: '',
status: undefined,
});
};
// 查看历史
const handleViewHistory = (record: WorkflowTemplateWithInstances) => {
setSelectedWorkflowDefinitionId(record.id);
setHistoryVisible(true);
};
// 终止流程TODO需要后端接口
const handleTerminate = async (record: WorkflowTemplateWithInstances) => {
if (!confirm(`确定要终止工作流 "${record.name}" 吗?`)) return;
console.log('终止流程', record);
// TODO: 调用终止接口
};
// 状态徽章
const getStatusBadge = (status: string) => {
const statusMap: Record<string, {
variant: 'default' | 'secondary' | 'destructive' | 'success' | 'outline';
text: string;
icon: React.ElementType
}> = {
NOT_STARTED: { variant: 'outline', text: '未启动', icon: Clock },
CREATED: { variant: 'secondary', text: '已创建', icon: PlayCircle },
RUNNING: { variant: 'default', text: '运行中', icon: Activity },
SUSPENDED: { variant: 'secondary', text: '已挂起', icon: Pause },
COMPLETED: { variant: 'success', text: '已完成', icon: CheckCircle2 },
TERMINATED: { variant: 'destructive', text: '已终止', icon: StopCircle },
FAILED: { variant: 'destructive', text: '失败', icon: XCircle },
};
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 runningCount = data?.content?.filter(d => d.lastExecutionStatus === 'RUNNING').length || 0;
const completedCount = data?.content?.filter(d => d.lastExecutionStatus === 'COMPLETED').length || 0;
const failedCount = data?.content?.filter(d => d.lastExecutionStatus === 'FAILED' || d.lastExecutionStatus === 'TERMINATED').length || 0;
return { total, runningCount, completedCount, failedCount };
}, [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-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>
<PlayCircle className="h-4 w-4 text-purple-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.runningCount}</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.completedCount}</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-red-500/10 to-red-500/5 border-red-500/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-red-700">/</CardTitle>
<XCircle className="h-4 w-4 text-red-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.failedCount}</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.status || undefined}
onValueChange={(value) => setQuery(prev => ({ ...prev, status: value }))}
>
<SelectTrigger className="w-[140px] h-9">
<SelectValue placeholder="全部状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="NOT_STARTED"></SelectItem>
<SelectItem value="CREATED"></SelectItem>
<SelectItem value="RUNNING"></SelectItem>
<SelectItem value="SUSPENDED"></SelectItem>
<SelectItem value="COMPLETED"></SelectItem>
<SelectItem value="TERMINATED"></SelectItem>
<SelectItem value="FAILED"></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 minWidth="850px">
<TableHeader>
<TableRow>
<TableHead width="200px"></TableHead>
<TableHead width="150px"></TableHead>
<TableHead width="180px"></TableHead>
<TableHead width="120px"></TableHead>
<TableHead width="200px" sticky></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={5} 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) => (
<TableRow key={record.id} className="hover:bg-muted/50">
<TableCell width="200px" className="font-medium">{record.name}</TableCell>
<TableCell width="150px">
{record.businessKey ? (
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold">
{record.businessKey}
</code>
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
<TableCell width="180px">
<span className="text-sm">
{record.lastExecutionTime
? dayjs(record.lastExecutionTime).format('YYYY-MM-DD HH:mm:ss')
: '-'}
</span>
</TableCell>
<TableCell width="120px">{getStatusBadge(record.lastExecutionStatus)}</TableCell>
<TableCell width="200px" sticky>
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleViewHistory(record)}
title="查看历史"
>
<Eye className="h-4 w-4" />
</Button>
{record.lastExecutionStatus === 'RUNNING' && (
<Button
variant="ghost"
size="sm"
onClick={() => handleTerminate(record)}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
title="终止流程"
>
<StopCircle className="h-4 w-4" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} 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 + 1}
pageSize={query.pageSize}
pageCount={pageCount}
onPageChange={(page) => setQuery(prev => ({
...prev,
pageNum: page - 1
}))}
/>
)}
</CardContent>
</Card>
{/* 历史记录弹窗 */}
<HistoryModal
visible={historyVisible}
onCancel={() => setHistoryVisible(false)}
workflowDefinitionId={selectedWorkflowDefinitionId}
/>
</div>
);
};
export default WorkflowInstanceList;