diff --git a/frontend/src/components/ui/alert.tsx b/frontend/src/components/ui/alert.tsx new file mode 100644 index 00000000..58d9b1b7 --- /dev/null +++ b/frontend/src/components/ui/alert.tsx @@ -0,0 +1,60 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } + diff --git a/frontend/src/pages/Dashboard/components/ApplicationCard.tsx b/frontend/src/pages/Dashboard/components/ApplicationCard.tsx index 84bfcbd7..bcaf9342 100644 --- a/frontend/src/pages/Dashboard/components/ApplicationCard.tsx +++ b/frontend/src/pages/Dashboard/components/ApplicationCard.tsx @@ -21,11 +21,12 @@ import { import { formatDuration, formatTime, getStatusIcon, getStatusText } from '../utils/dashboardUtils'; import type { ApplicationConfig, DeployEnvironment, DeployRecord } from '../types'; import { DeployFlowGraphModal } from './DeployFlowGraphModal'; +import { DeployConfirmDialog } from './DeployConfirmDialog'; interface ApplicationCardProps { app: ApplicationConfig; environment: DeployEnvironment; - onDeploy: (app: ApplicationConfig) => void; + onDeploy: (app: ApplicationConfig, remark: string) => void; isDeploying: boolean; } @@ -37,6 +38,7 @@ export const ApplicationCard: React.FC = ({ }) => { const [selectedDeployRecordId, setSelectedDeployRecordId] = useState(null); const [flowModalOpen, setFlowModalOpen] = useState(false); + const [deployDialogOpen, setDeployDialogOpen] = useState(false); const handleDeployRecordClick = (record: DeployRecord) => { setSelectedDeployRecordId(record.id); @@ -330,7 +332,7 @@ export const ApplicationCard: React.FC = ({ {/* 部署按钮 */} + {/* 部署确认对话框 */} + onDeploy(app, remark)} + applicationName={app.applicationName} + environmentName={environment.environmentName} + loading={isDeploying || app.isDeploying} + /> + {/* 部署流程图模态框 */} ; + +interface DeployConfirmDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onConfirm: (remark: string) => void; + applicationName: string; + environmentName: string; + loading?: boolean; +} + +export const DeployConfirmDialog: React.FC = ({ + open, + onOpenChange, + onConfirm, + applicationName, + environmentName, + loading = false, +}) => { + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + remark: '', + }, + }); + + // 当对话框打开/关闭时重置表单 + React.useEffect(() => { + if (open) { + form.reset(); + } + }, [open, form]); + + const onSubmit = (values: FormValues) => { + onConfirm(values.remark); + onOpenChange(false); + }; + + return ( + + + + + + 确认部署 + + + +
+ + + {/* 部署信息 */} +
+
+ 应用 + {applicationName} +
+
+ 环境 + {environmentName} +
+
+ + {/* 任务号输入 */} + ( + + + 部署任务号 * + + + + + + 格式:[task-数字][bugfix-数字] + 任务描述 + + {/* 错误信息容器 - 固定高度避免布局抖动 */} +
+ +
+
+ )} + /> +
+ + + + + +
+ +
+
+ ); +}; + diff --git a/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx b/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx index bce0bb52..031a0605 100644 --- a/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx +++ b/frontend/src/pages/Dashboard/components/DeployFlowGraphModal.tsx @@ -2,12 +2,36 @@ import React, { useEffect, useState, useMemo } from 'react'; import { ReactFlowProvider, ReactFlow, Background, Controls, MiniMap, Node, Edge, Handle, Position, BackgroundVariant } from '@xyflow/react'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Badge } from '@/components/ui/badge'; -import { Loader2 } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Progress } from '@/components/ui/progress'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Separator } from '@/components/ui/separator'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { + Loader2, + AlertCircle, + User, + Building2, + Layers, + Calendar, + Clock, + CheckCircle2, + XCircle, + AlertTriangle +} from 'lucide-react'; import { cn } from '@/lib/utils'; import '@xyflow/react/dist/style.css'; import { getDeployRecordFlowGraph } from '../service'; import type { DeployRecordFlowGraph, WorkflowNodeInstance } from '../types'; -import { getStatusIcon, getStatusText } from '../utils/dashboardUtils'; +import { + getStatusIcon, + getStatusText, + getNodeStatusText, + getNodeStatusColor, + formatTime, + formatDuration, + calculateRunningDuration +} from '../utils/dashboardUtils'; interface DeployFlowGraphModalProps { open: boolean; @@ -15,104 +39,328 @@ interface DeployFlowGraphModalProps { onOpenChange: (open: boolean) => void; } -// 节点状态颜色映射 -const nodeStatusColorMap: Record = { - NOT_STARTED: { - bg: '#fafafa', - border: '#d9d9d9', - text: '#666' - }, - RUNNING: { - bg: 'linear-gradient(135deg, #1890ff 0%, #40a9ff 100%)', - border: '#1890ff', - text: '#fff' - }, - COMPLETED: { - bg: 'linear-gradient(135deg, #52c41a 0%, #73d13d 100%)', - border: '#52c41a', - text: '#fff' - }, - FAILED: { - bg: 'linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%)', - border: '#ff4d4f', - text: '#fff' - }, - REJECTED: { - bg: 'linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%)', - border: '#ff4d4f', - text: '#fff' - }, - CANCELLED: { - bg: '#d9d9d9', - border: '#bfbfbf', - text: '#666' - }, - TERMINATED: { - bg: 'linear-gradient(135deg, #faad14 0%, #ffc53d 100%)', - border: '#faad14', - text: '#fff' - } -}; - -// 简版自定义节点组件(只显示节点名和状态) -const CustomFlowNode: React.FC = ({ data, selected }) => { - const status = data.status || 'NOT_STARTED'; - const colors = nodeStatusColorMap[status] || nodeStatusColorMap.NOT_STARTED; +/** + * 自定义流程节点组件 + */ +const CustomFlowNode: React.FC = ({ data }) => { + const { nodeName, nodeType, status, startTime, endTime, duration, errorMessage } = data; + const statusColor = getNodeStatusColor(status); const isNotStarted = status === 'NOT_STARTED'; const isRunning = status === 'RUNNING'; - const nodeType = data.nodeType; + const hasFailed = status === 'FAILED'; + + // 计算显示的时长 + const displayDuration = useMemo(() => { + if (duration !== null && duration !== undefined) { + // 后端返回的 duration 单位已经是毫秒 + return formatDuration(duration); + } + if (isRunning && startTime) { + const runningDuration = calculateRunningDuration(startTime); + return formatDuration(runningDuration); + } + return null; + }, [duration, isRunning, startTime]); + + const nodeContent = ( +
+ {/* 节点名称 */} +
{nodeName}
+ + {/* 节点状态 */} +
+ {getNodeStatusText(status)} +
+ + {/* 时间信息 */} + {!isNotStarted && ( +
+ {startTime &&
开始: {formatTime(startTime)}
} + {endTime &&
结束: {formatTime(endTime)}
} + {displayDuration && ( +
+ {isRunning ? '运行中: ' : '时长: '} + {displayDuration} +
+ )} +
+ )} + + {/* 错误提示 */} + {hasFailed && errorMessage && ( +
+ + 有错误 +
+ )} +
+ ); return ( -
+ <> {/* 输入连接点 */} {nodeType !== 'START_EVENT' && ( - + )} - {/* 节点内容 - 简化版 */} -
-
-
{data.nodeName}
-
{getStatusText(status)}
- {isRunning && } -
-
+ {/* 节点内容 - 如果有错误信息,包装在 Tooltip 中 */} + {hasFailed && errorMessage ? ( + + + + {nodeContent} + + +

错误信息:

+

{errorMessage}

+
+
+
+ ) : ( + nodeContent + )} {/* 输出连接点 */} {nodeType !== 'END_EVENT' && ( - + )} -
+ ); }; const nodeTypes = { - default: CustomFlowNode, + custom: CustomFlowNode, }; /** - * 部署流程图模态框 + * 业务信息卡片 + */ +const BusinessInfoCard: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => { + return ( + + + + + 业务信息 + + + +
+ 应用名称: + {flowData.applicationName} +
+
+ 应用编码: + {flowData.applicationCode} +
+ +
+ 部署环境: + {flowData.environmentName} +
+
+ 所属团队: + {flowData.teamName} +
+ +
+ + + 部署人: + + {flowData.deployBy} +
+ {flowData.deployRemark && ( + <> + +
+ 备注: +

{flowData.deployRemark}

+
+ + )} +
+
+ ); +}; + +/** + * 进度信息卡片 + */ +const ProgressCard: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => { + const { executedNodeCount, totalNodeCount, successNodeCount, failedNodeCount, runningNodeCount } = flowData; + const progress = totalNodeCount > 0 ? (executedNodeCount / totalNodeCount) * 100 : 0; + + return ( + + + + + 执行进度 + + + +
+
+ 已执行 + + {executedNodeCount} / {totalNodeCount} 节点 + +
+ +
+ {Math.round(progress)}% +
+
+ + + +
+
+ + 成功 + {successNodeCount} +
+
+ + 失败 + {failedNodeCount} +
+
+ + 运行中 + {runningNodeCount} +
+
+
+
+ ); +}; + +/** + * 时长统计卡片 + */ +const DurationCard: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => { + const { deployStartTime, deployDuration, nodeInstances, runningNodeCount } = flowData; + const isRunning = runningNodeCount > 0; + + // 计算当前总时长 + const currentDuration = useMemo(() => { + if (deployDuration) return deployDuration; // 后端返回的已经是毫秒 + if (isRunning) return calculateRunningDuration(deployStartTime); + return 0; + }, [deployDuration, isRunning, deployStartTime]); + + return ( + + + + + 时长统计 + + + +
+
+ + + 开始时间: + + + {deployStartTime ? formatTime(deployStartTime) : '-'} + +
+
+ 总时长: + + {formatDuration(currentDuration)} + {isRunning && (运行中...)} + +
+
+ + + +
+
节点执行时长:
+
+ {nodeInstances + .filter(ni => ni.status !== 'NOT_STARTED') + .map((ni) => { + // 后端返回的 duration 单位已经是毫秒 + const nodeDuration = ni.duration + ? ni.duration + : (ni.status === 'RUNNING' ? calculateRunningDuration(ni.startTime) : 0); + return ( +
+ + {ni.nodeName} + + + {formatDuration(nodeDuration)} + {ni.status === 'RUNNING' && '...'} + +
+ ); + })} +
+
+
+
+ ); +}; + +/** + * 实时监控提示 + */ +const MonitoringAlert: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => { + const { runningNodeCount } = flowData; + + if (runningNodeCount === 0) return null; + + return ( + + + + 部署正在进行中 + + 当前有 {runningNodeCount} 个节点正在执行,请稍候... + + + + ); +}; + +/** + * 信息面板(左侧边栏) + */ +const DeployInfoPanel: React.FC<{ flowData: DeployRecordFlowGraph }> = ({ flowData }) => { + return ( +
+ + + + +
+ ); +}; + +/** + * 部署流程图模态框(主组件) */ export const DeployFlowGraphModal: React.FC = ({ open, @@ -141,7 +389,7 @@ export const DeployFlowGraphModal: React.FC = ({ } }, [open, deployRecordId]); - // 创建节点状态映射(用于快速查找) + // 创建节点ID到实例的映射 const nodeInstanceMap = useMemo(() => { if (!flowData?.nodeInstances) return new Map(); const map = new Map(); @@ -151,196 +399,102 @@ export const DeployFlowGraphModal: React.FC = ({ return map; }, [flowData]); - // 获取节点状态(通过匹配 nodeInstances) - const getNodeStatus = (nodeId: string): string => { - const instance = nodeInstanceMap.get(nodeId); - return instance ? instance.status : 'NOT_STARTED'; - }; + // 转换为 React Flow 节点(按执行顺序重新布局) + const flowNodes: Node[] = useMemo(() => { + if (!flowData?.graph?.nodes || !flowData?.nodeInstances) return []; - // 计算节点拓扑顺序(用于自动布局) - const calculateNodeOrder = useMemo(() => { - if (!flowData?.graph?.nodes || !flowData?.graph?.edges) { - return new Map(); - } - - const edges = flowData.graph.edges; - const nodes = flowData.graph.nodes; - const nodeIds = new Set(nodes.map(n => n.id)); - - // 构建图的邻接表和入度 - const adjacencyList = new Map(); - const inDegree = new Map(); - - // 初始化 - nodes.forEach(node => { - adjacencyList.set(node.id, []); - inDegree.set(node.id, 0); - }); - - // 构建图 - edges.forEach(edge => { - const source = edge.from; - const target = edge.to; - if (nodeIds.has(source) && nodeIds.has(target)) { - adjacencyList.get(source)!.push(target); - inDegree.set(target, (inDegree.get(target) || 0) + 1); - } - }); - - // 拓扑排序:找到所有没有入边的节点(起始节点) - const queue: string[] = []; - nodes.forEach(node => { - if (inDegree.get(node.id) === 0) { - queue.push(node.id); - } - }); - - // 拓扑排序 - const topoOrder: string[] = []; - const processed = new Set(); - let currentLevelNodes = [...queue]; - - while (currentLevelNodes.length > 0) { - const nextLevelNodes: string[] = []; - - currentLevelNodes.forEach(nodeId => { - if (processed.has(nodeId)) return; - processed.add(nodeId); - topoOrder.push(nodeId); - - // 找到所有从当前节点出发的边,减少目标节点的入度 - adjacencyList.get(nodeId)?.forEach(targetId => { - const currentInDegree = (inDegree.get(targetId) || 0) - 1; - inDegree.set(targetId, currentInDegree); - - if (currentInDegree === 0 && !processed.has(targetId)) { - nextLevelNodes.push(targetId); - } - }); + // 按执行顺序排序已执行的节点 + const executedInstances = flowData.nodeInstances + .filter(ni => ni.status !== 'NOT_STARTED') + .sort((a, b) => { + const timeA = a.startTime ? new Date(a.startTime.replace(' ', 'T')).getTime() : 0; + const timeB = b.startTime ? new Date(b.startTime.replace(' ', 'T')).getTime() : 0; + return timeA - timeB; }); - currentLevelNodes = nextLevelNodes; - } + // 创建节点位置映射(已执行的节点) + const nodePositionMap = new Map(); + const horizontalSpacing = 250; + const startX = 50; + const startY = 150; - // 处理剩余节点 - nodes.forEach(node => { - if (!processed.has(node.id)) { - topoOrder.push(node.id); - } + executedInstances.forEach((instance, index) => { + nodePositionMap.set(instance.nodeId, { + x: startX + index * horizontalSpacing, + y: startY, + }); }); - // 创建顺序映射 - const orderMap = new Map(); - topoOrder.forEach((nodeId, index) => { - orderMap.set(nodeId, index); - }); - - return orderMap; - }, [flowData]); - - // 转换为 React Flow 节点(自动水平布局,简化版) - const flowNodes: Node[] = useMemo(() => { - if (!flowData?.graph?.nodes) return []; - - // 按拓扑顺序排序节点 - const sortedNodes = Array.from(flowData.graph.nodes).sort((a, b) => { - const orderA = calculateNodeOrder.get(a.id) ?? Infinity; - const orderB = calculateNodeOrder.get(b.id) ?? Infinity; - return orderA - orderB; - }); - - // 自动水平排列(紧凑布局) - const horizontalSpacing = 150; // 缩小间距 - const startX = 100; - const startY = 200; // 固定Y坐标,所有节点在同一行 - - return sortedNodes.map((node, index) => { - // 1. 匹配执行状态 + // 为所有节点生成位置(包括未执行的) + return flowData.graph.nodes.map((node) => { const instance = nodeInstanceMap.get(node.id); - const status = instance ? instance.status : 'NOT_STARTED'; - - const colors = nodeStatusColorMap[status] || nodeStatusColorMap.NOT_STARTED; + + // 如果节点已执行,使用计算的位置;否则使用原始位置但偏移到下方 + let position = nodePositionMap.get(node.id); + if (!position) { + // 未执行的节点放在下方,使用原始相对位置 + position = { + x: node.position.x + 500, + y: node.position.y + 400, + }; + } return { id: node.id, - type: 'default', - // 自动水平布局 - position: { - x: startX + index * horizontalSpacing, - y: startY, - }, + type: 'custom', + position, data: { nodeName: node.nodeName, nodeType: node.nodeType, - status: status, - }, - style: { - background: colors.bg, - borderColor: colors.border, - color: colors.text, + status: instance?.status || 'NOT_STARTED', + startTime: instance?.startTime, + endTime: instance?.endTime, + duration: instance?.duration, + errorMessage: instance?.errorMessage, }, }; }); - }, [flowData, nodeInstanceMap, calculateNodeOrder]); + }, [flowData, nodeInstanceMap]); - // 判断边是否已执行 - const isEdgeExecuted = (sourceNodeId: string): boolean => { - const sourceStatus = getNodeStatus(sourceNodeId); - return sourceStatus === 'COMPLETED' || sourceStatus === 'RUNNING'; - }; - - // 判断边是否中断 - const isEdgeInterrupted = (sourceNodeId: string): boolean => { - const sourceStatus = getNodeStatus(sourceNodeId); - return sourceStatus === 'FAILED' || sourceStatus === 'REJECTED'; - }; - - // 渲染连线(使用 edge.from 和 edge.to) + // 转换为 React Flow 边 const flowEdges: Edge[] = useMemo(() => { if (!flowData?.graph?.edges) return []; - const edges: Edge[] = []; - flowData.graph.edges.forEach((edge, index) => { - // 使用 edge.from 和 edge.to + return flowData.graph.edges.map((edge, index) => { const source = edge.from; const target = edge.to; - - if (!source || !target) { - console.warn('边数据不完整:', edge); - return; + const sourceInstance = nodeInstanceMap.get(source); + const sourceStatus = sourceInstance?.status || 'NOT_STARTED'; + + // 根据源节点状态确定边的颜色 + let strokeColor = '#d1d5db'; // 默认灰色 + if (sourceStatus === 'COMPLETED') { + strokeColor = '#10b981'; // 绿色 + } else if (sourceStatus === 'FAILED') { + strokeColor = '#ef4444'; // 红色 + } else if (sourceStatus === 'RUNNING') { + strokeColor = '#3b82f6'; // 蓝色 } - const executed = isEdgeExecuted(source); - const interrupted = isEdgeInterrupted(source); - const targetStatus = getNodeStatus(target); - const isDashed = !executed || targetStatus === 'NOT_STARTED'; - - let strokeColor = '#d9d9d9'; - if (interrupted) { - strokeColor = '#ff4d4f'; - } else if (executed) { - strokeColor = '#52c41a'; - } - - edges.push({ - id: `edge-${source}-${target}-${index}`, + return { + id: edge.id || `edge-${source}-${target}-${index}`, source, target, - type: 'smoothstep' as const, - animated: executed && targetStatus === 'RUNNING', + type: 'smoothstep', + // 不显示边的标签(条件表达式等) + animated: sourceStatus === 'RUNNING', style: { stroke: strokeColor, - strokeWidth: executed ? 3 : 2, - strokeDasharray: isDashed ? '5,5' : undefined, + strokeWidth: 2, }, markerEnd: { - type: 'arrowclosed' as const, + type: 'arrowclosed', color: strokeColor, + width: 20, + height: 20, }, - }); + }; }); - - return edges; }, [flowData, nodeInstanceMap]); // 获取部署状态信息 @@ -357,9 +511,14 @@ export const DeployFlowGraphModal: React.FC = ({ return ( - + + {flowData && ( + + #{flowData.deployRecordId} + + )} 部署流程图 {deployStatusInfo && ( = ({ {deployStatusInfo.text} )} - {flowData && ( - - 记录ID: #{flowData.deployRecordId} - - )} -
+
{loading ? ( -
+

加载流程图数据...

) : flowData ? ( - - - - - { - const status = node.data?.status || 'NOT_STARTED'; - const colors = nodeStatusColorMap[status] || nodeStatusColorMap.NOT_STARTED; - return colors.border; - }} - className="bg-background border" - /> - - + <> + {/* 左侧信息面板 */} + + + {/* 右侧流程图可视化 */} +
+ + + + + { + const status = node.data?.status || 'NOT_STARTED'; + return getNodeStatusColor(status); + }} + className="bg-background/80 backdrop-blur-sm border shadow-sm" + pannable={true} + zoomable={true} + /> + + +
+ ) : ( -
-

暂无流程图数据

+
+
+ +

暂无流程图数据

+
)}
@@ -422,4 +599,3 @@ export const DeployFlowGraphModal: React.FC = ({
); }; - diff --git a/frontend/src/pages/Dashboard/components/PendingApprovalModal.tsx b/frontend/src/pages/Dashboard/components/PendingApprovalModal.tsx new file mode 100644 index 00000000..ad00b56a --- /dev/null +++ b/frontend/src/pages/Dashboard/components/PendingApprovalModal.tsx @@ -0,0 +1,383 @@ +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogBody, + DialogLoading, +} 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 { Badge } from "@/components/ui/badge"; +import { Textarea } from "@/components/ui/textarea"; +import { + ClipboardCheck, + User, + Calendar, + FileText, + Package, + Clock, + CheckCircle, + XCircle, +} from "lucide-react"; +import { useToast } from '@/components/ui/use-toast'; +import { getMyApprovalTasks, completeApproval } from '../service'; +import { formatTime, formatDuration } from '../utils/dashboardUtils'; +import { ApprovalResult, type PendingApprovalTask } from '../types'; +import { DeployFlowGraphModal } from './DeployFlowGraphModal'; + +interface PendingApprovalModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export const PendingApprovalModal: React.FC = ({ + open, + onOpenChange, +}) => { + const { toast } = useToast(); + const [loading, setLoading] = useState(false); + const [approvalList, setApprovalList] = useState([]); + const [selectedDeployRecordId, setSelectedDeployRecordId] = useState(null); + const [flowModalOpen, setFlowModalOpen] = useState(false); + const [approvalDialogOpen, setApprovalDialogOpen] = useState(false); + const [selectedTask, setSelectedTask] = useState(null); + const [approvalResult, setApprovalResult] = useState(null); + const [approvalComment, setApprovalComment] = useState(''); + const [submitting, setSubmitting] = useState(false); + + // 加载待审批列表 + const loadApprovalList = async () => { + try { + setLoading(true); + const response = await getMyApprovalTasks(); + if (response) { + setApprovalList(response || []); + } + } catch (error: any) { + toast({ + variant: 'destructive', + title: '加载失败', + description: error.response?.data?.message || '获取待审批列表失败', + }); + } finally { + setLoading(false); + } + }; + + // 当弹窗打开时加载数据 + useEffect(() => { + if (open) { + loadApprovalList(); + } + }, [open]); + + // 处理查看详情 + const handleViewDetail = (task: PendingApprovalTask) => { + setSelectedDeployRecordId(task.deployRecordId); + setFlowModalOpen(true); + }; + + // 打开审批对话框 + const handleOpenApproval = (task: PendingApprovalTask, result: ApprovalResult) => { + setSelectedTask(task); + setApprovalResult(result); + setApprovalComment(''); + setApprovalDialogOpen(true); + }; + + // 提交审批 + const handleSubmitApproval = async () => { + if (!selectedTask || !approvalResult) return; + + // 如果是必须填写意见且没有填写,提示用户 + if (selectedTask.requireComment && !approvalComment.trim()) { + toast({ + variant: 'destructive', + title: '请填写审批意见', + description: '该审批任务要求必须填写意见', + }); + return; + } + + try { + setSubmitting(true); + await completeApproval({ + taskId: selectedTask.taskId, + result: approvalResult, + comment: approvalComment.trim() || undefined, + }); + + toast({ + title: '审批成功', + description: approvalResult === ApprovalResult.APPROVED ? '已通过该部署申请' : '已拒绝该部署申请', + }); + + // 关闭审批对话框 + setApprovalDialogOpen(false); + + // 重新加载列表 + await loadApprovalList(); + } catch (error: any) { + toast({ + variant: 'destructive', + title: '审批失败', + description: error.response?.data?.message || '提交审批失败,请稍后重试', + }); + } finally { + setSubmitting(false); + } + }; + + return ( + <> + + + + + + 待审批列表 + {!loading && ( + + {approvalList.length} 条待审批 + + )} + + + + + {loading ? ( + + ) : approvalList.length === 0 ? ( + // 空状态 +
+
+ +
+

暂无待审批任务

+

+ 当前没有需要您审批的部署申请 +

+
+ ) : ( + // 列表内容 +
+ {approvalList.map((task) => ( +
+ {/* 左侧内容区 */} +
+ {/* 应用图标 */} +
+
+ +
+
+ + {/* 主要信息 */} +
+ {/* 标题行 */} +
+
+
+

+ {task.applicationName} +

+ + #{task.deployRecordId} + +
+
+ + {task.applicationCode} + + + {task.environmentName} + + {task.teamName} +
+
+ + {/* 右侧操作按钮 */} +
+ + +
+
+ + {/* 审批内容 */} + {(task.approvalTitle || task.approvalContent) && ( +
+ {task.approvalTitle && ( +
+ {task.approvalTitle} +
+ )} + {task.approvalContent && ( +
+ {task.approvalContent} +
+ )} +
+ )} + + {/* 底部信息栏 */} +
+
+
+ + {task.deployBy} +
+
+ + {formatTime(task.deployStartTime)} +
+ {task.pendingDuration && ( +
+ + 等待 {formatDuration(task.pendingDuration)} +
+ )} +
+ +
+ + {/* 备注(如果有) */} + {task.deployRemark && ( +
+ + {task.deployRemark} +
+ )} +
+
+
+ ))} +
+ )} +
+
+
+ + {/* 审批确认对话框 */} + + + + + {approvalResult === ApprovalResult.APPROVED ? ( + <> + + 确认通过审批 + + ) : ( + <> + + 确认拒绝审批 + + )} + + + {selectedTask && ( + <> +
+ 您即将{approvalResult === ApprovalResult.APPROVED ? '通过' : '拒绝'}以下部署申请: +
+
+
+ 记录ID + #{selectedTask.deployRecordId} +
+
+ 应用 + {selectedTask.applicationName} +
+
+ 环境 + {selectedTask.environmentName} +
+
+ 申请人 + {selectedTask.deployBy} +
+
+ +
+ +