This commit is contained in:
dengqichen 2025-10-23 18:35:20 +08:00
parent 3003bb6abe
commit 0ca4aa58b7
7 changed files with 491 additions and 163 deletions

View File

@ -24,9 +24,11 @@
"@radix-ui/react-navigation-menu": "^1.2.3", "@radix-ui/react-navigation-menu": "^1.2.3",
"@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.1.4", "@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
@ -57,6 +59,7 @@
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",
"react-redux": "^9.0.4", "react-redux": "^9.0.4",
"react-router-dom": "^6.21.0", "react-router-dom": "^6.21.0",
"reactflow": "^11.11.4",
"recharts": "^2.15.0", "recharts": "^2.15.0",
"rsuite": "^5.83.3", "rsuite": "^5.83.3",
"uuid": "^13.0.0", "uuid": "^13.0.0",

View File

@ -0,0 +1,44 @@
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-3.5 w-3.5 fill-primary" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

View File

@ -0,0 +1,26 @@
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }

View File

@ -1,7 +1,15 @@
import React from 'react'; import React, { useMemo } from 'react';
import { Modal, Steps, Card, Descriptions, Tag, Timeline } from 'antd'; import { Dialog, DialogPortal, DialogOverlay, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { WorkflowHistoricalInstance, WorkflowInstanceStage } from '../types'; import { WorkflowHistoricalInstance, WorkflowInstanceStage } from '../types';
import ReactFlow, { Background, Controls, MiniMap, Node, Edge } from 'reactflow';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import 'reactflow/dist/style.css';
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X, Clock, CheckCircle2, XCircle, AlertCircle } from "lucide-react";
import { cn } from "@/lib/utils";
interface DetailModalProps { interface DetailModalProps {
visible: boolean; visible: boolean;
@ -12,104 +20,345 @@ interface DetailModalProps {
const DetailModal: React.FC<DetailModalProps> = ({ visible, onCancel, instanceData }) => { const DetailModal: React.FC<DetailModalProps> = ({ visible, onCancel, instanceData }) => {
if (!instanceData) return null; if (!instanceData) return null;
const getStatusTag = (status: string) => { const getStatusBadge = (status: string) => {
const statusMap: Record<string, { color: string; text: string }> = { const statusMap: Record<string, { variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'success'; text: string }> = {
COMPLETED: { color: 'success', text: '已完成' }, COMPLETED: { variant: 'success', text: '已完成' },
RUNNING: { color: 'processing', text: '运行中' }, RUNNING: { variant: 'default', text: '运行中' },
FAILED: { color: 'error', text: '失败' }, FAILED: { variant: 'destructive', text: '失败' },
TERMINATED: { color: 'warning', text: '已终止' }, TERMINATED: { variant: 'secondary', text: '已终止' },
NOT_STARTED: { color: 'default', text: '未执行' } NOT_STARTED: { variant: 'outline', text: '未执行' }
}; };
const statusInfo = statusMap[status] || { color: 'default', text: status }; const statusInfo = statusMap[status] || { variant: 'outline', text: status };
return <Tag color={statusInfo.color}>{statusInfo.text}</Tag>; return <Badge variant={statusInfo.variant}>{statusInfo.text}</Badge>;
}; };
const getNodeTypeText = (nodeType: string) => { const getNodeTypeText = (nodeType: string) => {
const nodeTypeMap: Record<string, string> = { const nodeTypeMap: Record<string, string> = {
startEvent: '开始节点', START_EVENT: '开始节点',
endEvent: '结束节点', END_EVENT: '结束节点',
serviceTask: '服务任务', SERVICE_TASK: '服务任务',
userTask: '用户任务', USER_TASK: '用户任务',
scriptTask: '脚本任务' SCRIPT_TASK: '脚本任务',
NOTIFICATION: '通知任务',
JENKINS_BUILD: 'Jenkins构建',
APPROVAL: '审批任务',
StartEvent: '开始节点',
EndEvent: '结束节点',
ServiceTask: '服务任务',
UserTask: '用户任务',
ScriptTask: '脚本任务'
}; };
return nodeTypeMap[nodeType] || nodeType; return nodeTypeMap[nodeType] || nodeType;
}; };
const getStepStatus = (stage: WorkflowInstanceStage) => { const getStatusIcon = (status: string) => {
switch (stage.status) { switch (status) {
case 'COMPLETED': case 'COMPLETED':
return 'finish'; return <CheckCircle2 className="h-4 w-4 text-green-500" />;
case 'RUNNING': case 'RUNNING':
return 'process'; return <Clock className="h-4 w-4 text-blue-500 animate-pulse" />;
case 'FAILED': case 'FAILED':
return 'error'; return <XCircle className="h-4 w-4 text-red-500" />;
case 'TERMINATED':
return 'wait';
default: default:
return 'wait'; return <AlertCircle className="h-4 w-4 text-gray-400" />;
} }
}; };
return ( // 构建节点状态映射
<Modal const nodeStatusMap = useMemo(() => {
title="流程执行详情" const map = new Map<string, WorkflowInstanceStage>();
open={visible} instanceData.stages.forEach(stage => {
onCancel={onCancel} map.set(stage.nodeId, stage);
width={1200} });
footer={null} return map;
> }, [instanceData.stages]);
<Card className="mb-4">
<Descriptions title="基本信息" bordered column={2}>
<Descriptions.Item label="业务标识">{instanceData.businessKey}</Descriptions.Item>
<Descriptions.Item label="状态">{getStatusTag(instanceData.status)}</Descriptions.Item>
<Descriptions.Item label="开始时间">{dayjs(instanceData.startTime).format('YYYY-MM-DD HH:mm:ss')}</Descriptions.Item>
<Descriptions.Item label="结束时间">
{instanceData.endTime ? dayjs(instanceData.endTime).format('YYYY-MM-DD HH:mm:ss') : '暂无'}
</Descriptions.Item>
<Descriptions.Item label="流程实例ID">{instanceData.processInstanceId}</Descriptions.Item>
<Descriptions.Item label="流程定义ID">{instanceData.processDefinitionId}</Descriptions.Item>
</Descriptions>
</Card>
<Card title="执行阶段" className="mb-4"> // 获取节点状态
<Steps const getNodeStatus = (nodeId: string): string => {
progressDot const stage = nodeStatusMap.get(nodeId);
current={instanceData.stages.length} return stage?.status || 'NOT_STARTED';
items={instanceData.stages.map((stage) => ({ };
title: stage.nodeName,
description: ( // 获取节点颜色
<div className="text-xs"> const getNodeColor = (status: string) => {
<div>{getNodeTypeText(stage.nodeType)}</div> const colorMap: Record<string, string> = {
<div>{getStatusTag(stage.status)}</div> COMPLETED: '#52c41a',
RUNNING: '#1890ff',
FAILED: '#ff4d4f',
TERMINATED: '#faad14',
NOT_STARTED: '#d9d9d9'
};
return colorMap[status] || '#d9d9d9';
};
// 转换为React Flow节点
const flowNodes: Node[] = useMemo(() => {
if (!instanceData.graph?.nodes) return [];
return instanceData.graph.nodes.map(node => {
const status = getNodeStatus(node.id);
const stage = nodeStatusMap.get(node.id);
return {
id: node.id,
type: 'default',
position: { x: node.position.x + 400, y: node.position.y + 200 },
data: {
label: (
<div style={{ textAlign: 'center', minWidth: '80px' }}>
<div style={{
fontSize: '12px',
fontWeight: 500,
marginBottom: '4px'
}}>
{node.nodeName}
</div> </div>
), <div style={{ fontSize: '10px', color: '#666' }}>
status: getStepStatus(stage) {getNodeTypeText(node.nodeType)}
}))} </div>
/> {stage && (
</Card> <div style={{ fontSize: '10px', marginTop: '4px' }}>
{getStatusBadge(status)}
<Card title="详细时间线">
<Timeline
items={instanceData.stages.map((stage) => ({
color: stage.status === 'COMPLETED' ? 'green' :
stage.status === 'FAILED' ? 'red' :
stage.status === 'RUNNING' ? 'blue' : 'gray',
children: (
<div>
<div className="font-medium">{stage.nodeName}</div>
{stage.id && (
<div className="text-gray-500 text-sm">
{dayjs(stage.startTime).format('YYYY-MM-DD HH:mm:ss')}
{stage.endTime && ` - ${dayjs(stage.endTime).format('YYYY-MM-DD HH:mm:ss')}`}
</div> </div>
)} )}
<div>{getStatusTag(stage.status)}</div>
</div> </div>
) )
}))} },
style: {
background: getNodeColor(status),
color: status === 'NOT_STARTED' ? '#333' : '#fff',
border: `2px solid ${getNodeColor(status)}`,
borderRadius: '8px',
padding: '10px',
minWidth: '120px',
boxShadow: status === 'RUNNING' ? '0 0 10px rgba(24, 144, 255, 0.5)' : undefined
}
};
});
}, [instanceData.graph, nodeStatusMap]);
// 判断连线是否已执行
const isEdgeExecuted = (fromNodeId: string, _toNodeId: string): boolean => {
const fromStage = nodeStatusMap.get(fromNodeId);
// 如果起始节点已完成,则连线已执行
return fromStage?.status === 'COMPLETED' || fromStage?.status === 'RUNNING';
};
// 转换为React Flow边
const flowEdges: Edge[] = useMemo(() => {
if (!instanceData.graph?.edges) return [];
return instanceData.graph.edges.map(edge => {
const executed = isEdgeExecuted(edge.from, edge.to);
return {
id: edge.id,
source: edge.from,
target: edge.to,
label: edge.name || undefined,
type: 'smoothstep',
animated: isEdgeExecuted(edge.from, edge.to) && getNodeStatus(edge.to) === 'RUNNING',
style: {
stroke: executed ? '#52c41a' : '#d9d9d9',
strokeWidth: 2
},
labelStyle: {
fontSize: 10,
fill: '#666'
}
};
});
}, [instanceData.graph, nodeStatusMap]);
// 描述信息组件
const DescriptionItem = ({ label, value }: { label: string; value: React.ReactNode }) => (
<div className="flex items-start py-2 border-b last:border-b-0">
<div className="w-32 text-sm font-medium text-muted-foreground flex-shrink-0">{label}</div>
<div className="flex-1 text-sm">{value}</div>
</div>
);
return (
<Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
<DialogPortal>
<DialogOverlay className="z-[60]" />
<DialogPrimitive.Content
className={cn(
"fixed left-[50%] top-[50%] z-[60] grid w-full max-w-7xl translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg max-h-[90vh] overflow-y-auto"
)}
>
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<Tabs defaultValue="graph" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="graph"></TabsTrigger>
<TabsTrigger value="timeline">线</TabsTrigger>
<TabsTrigger value="info"></TabsTrigger>
</TabsList>
{/* 流程图标签页 */}
<TabsContent value="graph" className="space-y-4">
<Card>
<CardContent className="pt-6">
<div className="grid grid-cols-2 gap-4">
<DescriptionItem label="业务标识" value={instanceData.businessKey} />
<DescriptionItem label="状态" value={getStatusBadge(instanceData.status)} />
<DescriptionItem
label="开始时间"
value={dayjs(instanceData.startTime).format('YYYY-MM-DD HH:mm:ss')}
/> />
<DescriptionItem
label="结束时间"
value={instanceData.endTime ? dayjs(instanceData.endTime).format('YYYY-MM-DD HH:mm:ss') : '暂无'}
/>
</div>
</CardContent>
</Card> </Card>
</Modal>
{instanceData.graph ? (
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div style={{ height: '400px', border: '1px solid hsl(var(--border))', borderRadius: '8px' }}>
<ReactFlow
nodes={flowNodes}
edges={flowEdges}
fitView
minZoom={0.5}
maxZoom={1.5}
defaultViewport={{ x: 0, y: 0, zoom: 0.8 }}
>
<Background />
<Controls />
<MiniMap
nodeColor={(node: Node) => {
const status = getNodeStatus(node.id);
return getNodeColor(status);
}}
/>
</ReactFlow>
</div>
<div className="mt-4 flex gap-4 justify-center text-sm">
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-green-500 rounded" />
<span></span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-blue-500 rounded" />
<span></span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-red-500 rounded" />
<span></span>
</div>
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-gray-300 rounded" />
<span></span>
</div>
</div>
</CardContent>
</Card>
) : (
<Card>
<CardContent className="pt-6">
<div className="text-center py-10 text-muted-foreground">
</div>
</CardContent>
</Card>
)}
</TabsContent>
{/* 执行时间线标签页 */}
<TabsContent value="timeline">
<Card>
<CardContent className="pt-6">
<div className="relative space-y-4">
{instanceData.stages.map((stage, index) => (
<div key={stage.id || index} className="flex gap-4">
{/* 时间线图标 */}
<div className="flex flex-col items-center">
<div className="flex items-center justify-center w-8 h-8 rounded-full border-2 bg-background z-10"
style={{
borderColor: stage.status === 'COMPLETED' ? '#52c41a' :
stage.status === 'FAILED' ? '#ff4d4f' :
stage.status === 'RUNNING' ? '#1890ff' : '#d9d9d9'
}}
>
{getStatusIcon(stage.status)}
</div>
{index < instanceData.stages.length - 1 && (
<div className="w-0.5 h-full bg-border mt-1" />
)}
</div>
{/* 时间线内容 */}
<div className="flex-1 pb-8">
<div className="font-medium mb-1 flex items-center gap-2">
{stage.nodeName}
<span className="text-xs text-muted-foreground font-normal">
{getNodeTypeText(stage.nodeType)}
</span>
</div>
<div className="text-sm text-muted-foreground mb-2">
{stage.startTime && dayjs(stage.startTime).format('YYYY-MM-DD HH:mm:ss')}
{stage.endTime && `${dayjs(stage.endTime).format('HH:mm:ss')}`}
{stage.startTime && stage.endTime && (
<span className="ml-2">
(: {dayjs(stage.endTime).diff(dayjs(stage.startTime), 'second')})
</span>
)}
</div>
<div>{getStatusBadge(stage.status)}</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</TabsContent>
{/* 详细信息标签页 */}
<TabsContent value="info">
<Card>
<CardContent className="pt-6">
<div className="space-y-0">
<DescriptionItem label="流程实例ID" value={instanceData.processInstanceId} />
<DescriptionItem label="流程定义ID" value={instanceData.processDefinitionId} />
<DescriptionItem label="业务标识" value={instanceData.businessKey} />
<DescriptionItem label="状态" value={getStatusBadge(instanceData.status)} />
<DescriptionItem
label="开始时间"
value={dayjs(instanceData.startTime).format('YYYY-MM-DD HH:mm:ss')}
/>
<DescriptionItem
label="结束时间"
value={instanceData.endTime ? dayjs(instanceData.endTime).format('YYYY-MM-DD HH:mm:ss') : '暂无'}
/>
{instanceData.startTime && instanceData.endTime && (
<DescriptionItem
label="总耗时"
value={`${dayjs(instanceData.endTime).diff(dayjs(instanceData.startTime), 'second')}`}
/>
)}
<DescriptionItem label="执行节点数" value={instanceData.stages.length} />
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</DialogPrimitive.Content>
</DialogPortal>
</Dialog>
); );
}; };

View File

@ -1,6 +1,9 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Modal, Table, Tag, Button } from 'antd'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import type { ColumnsType } from 'antd/es/table'; import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { DataTablePagination } from '@/components/ui/pagination';
import { WorkflowHistoricalInstance } from '../types'; import { WorkflowHistoricalInstance } from '../types';
import { getHistoricalInstances } from '../service'; import { getHistoricalInstances } from '../service';
import DetailModal from './DetailModal'; import DetailModal from './DetailModal';
@ -54,86 +57,89 @@ const HistoryModal: React.FC<HistoryModalProps> = ({ visible, onCancel, workflow
setDetailVisible(true); setDetailVisible(true);
}; };
const getStatusTag = (status: string) => { const getStatusBadge = (status: string) => {
const statusMap: Record<string, { color: string; text: string }> = { const statusMap: Record<string, { variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'success'; text: string }> = {
COMPLETED: { color: 'success', text: '已完成' }, COMPLETED: { variant: 'success', text: '已完成' },
RUNNING: { color: 'processing', text: '运行中' }, RUNNING: { variant: 'default', text: '运行中' },
FAILED: { color: 'error', text: '失败' }, FAILED: { variant: 'destructive', text: '失败' },
TERMINATED: { color: 'warning', text: '已终止' } TERMINATED: { variant: 'secondary', text: '已终止' }
}; };
const statusInfo = statusMap[status] || { color: 'default', text: status }; const statusInfo = statusMap[status] || { variant: 'outline', text: status };
return <Tag color={statusInfo.color}>{statusInfo.text}</Tag>; return <Badge variant={statusInfo.variant}>{statusInfo.text}</Badge>;
}; };
const columns: ColumnsType<WorkflowHistoricalInstance> = [ const pageCount = Math.ceil(total / query.pageSize);
{
title: '业务标识',
dataIndex: 'businessKey',
key: 'businessKey',
width: 200,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: status => getStatusTag(status)
},
{
title: '开始时间',
dataIndex: 'startTime',
key: 'startTime',
width: 180,
},
{
title: '结束时间',
dataIndex: 'endTime',
key: 'endTime',
width: 180,
render: time => time || '暂无'
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 100,
render: (_, record) => (
<Button type="link" onClick={() => handleViewDetail(record)}>
</Button>
),
},
];
return ( return (
<> <>
<Modal <Dialog open={visible} onOpenChange={(open) => !open && onCancel()}>
title="历史执行记录" <DialogContent className="max-w-6xl max-h-[85vh] overflow-y-auto">
open={visible} <DialogHeader>
onCancel={onCancel} <DialogTitle></DialogTitle>
width={1000} </DialogHeader>
footer={null}
<div className="mt-4">
{loading ? (
<div className="flex justify-center items-center py-8">
<div className="text-muted-foreground">...</div>
</div>
) : data.length === 0 ? (
<div className="flex justify-center items-center py-8">
<div className="text-muted-foreground"></div>
</div>
) : (
<>
<div className="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[200px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[180px]"></TableHead>
<TableHead className="w-[180px]"></TableHead>
<TableHead className="w-[100px] text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((record) => (
<TableRow key={record.id}>
<TableCell className="font-medium">{record.businessKey}</TableCell>
<TableCell>{getStatusBadge(record.status)}</TableCell>
<TableCell>{record.startTime}</TableCell>
<TableCell>{record.endTime || '暂无'}</TableCell>
<TableCell className="text-right">
<Button
variant="link"
size="sm"
onClick={() => handleViewDetail(record)}
> >
<Table
columns={columns} </Button>
dataSource={data} </TableCell>
loading={loading} </TableRow>
rowKey="id" ))}
scroll={{ x: 800 }} </TableBody>
pagination={{ </Table>
current: query.pageNum + 1, </div>
pageSize: query.pageSize,
total: total, {/* 分页器 */}
onChange: (page, pageSize) => setQuery(prev => ({ {pageCount > 1 && (
<DataTablePagination
pageIndex={query.pageNum + 1}
pageSize={query.pageSize}
pageCount={pageCount}
onPageChange={(page) => setQuery(prev => ({
...prev, ...prev,
pageNum: page - 1, pageNum: page - 1
pageSize }))}
})),
showSizeChanger: true,
showQuickJumper: true,
}}
/> />
</Modal> )}
</>
)}
</div>
</DialogContent>
</Dialog>
{selectedInstance && ( {selectedInstance && (
<DetailModal <DetailModal
visible={detailVisible} visible={detailVisible}