重构消息通知弹窗

This commit is contained in:
dengqichen 2025-11-15 11:26:01 +08:00
parent 88f5ec1d5c
commit 96517b809e
4 changed files with 245 additions and 82 deletions

View File

@ -95,6 +95,7 @@ export const PendingApprovalModal: React.FC<PendingApprovalModalProps> = ({
// 打开审批对话框 // 打开审批对话框
const handleOpenApproval = (task: PendingApprovalTask, result: ApprovalResult) => { const handleOpenApproval = (task: PendingApprovalTask, result: ApprovalResult) => {
console.log('打开审批对话框,任务数据:', task); // 调试日志
setSelectedTask(task); setSelectedTask(task);
setApprovalResult(result); setApprovalResult(result);
setApprovalComment(''); setApprovalComment('');
@ -180,38 +181,41 @@ export const PendingApprovalModal: React.FC<PendingApprovalModalProps> = ({
{approvalList.map((task) => ( {approvalList.map((task) => (
<div <div
key={task.taskId} key={task.taskId}
className="group relative p-4 border rounded-lg hover:border-primary/50 hover:shadow-md transition-all bg-card" className="group relative p-4 border border-gray-200/80 rounded-xl hover:border-orange-300 hover:shadow-lg transition-all duration-300 bg-gradient-to-br from-white to-orange-50/30 overflow-hidden"
> >
{/* 装饰性渐变背景 */}
<div className="absolute top-0 right-0 w-24 h-24 bg-gradient-to-br from-orange-100/40 to-transparent rounded-full blur-2xl -z-0" />
{/* 左侧内容区 */} {/* 左侧内容区 */}
<div className="flex gap-4"> <div className="flex gap-3 relative z-10">
{/* 应用图标 */} {/* 应用图标 */}
<div className="shrink-0"> <div className="shrink-0">
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-primary/20 to-primary/5 flex items-center justify-center"> <div className="w-12 h-12 rounded-lg bg-gradient-to-br from-orange-500 to-orange-600 flex items-center justify-center shadow-lg shadow-orange-500/30">
<Package className="h-6 w-6 text-primary" /> <Package className="h-6 w-6 text-white" />
</div> </div>
</div> </div>
{/* 主要信息 */} {/* 主要信息 */}
<div className="flex-1 min-w-0 space-y-2"> <div className="flex-1 min-w-0 space-y-1.5">
{/* 标题行 */} {/* 标题行 */}
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<h3 className="text-base font-bold text-foreground truncate"> <h3 className="text-base font-bold text-gray-900 truncate">
{task.applicationName} {task.applicationName}
</h3> </h3>
<span className="shrink-0 text-xs px-1.5 py-0.5 bg-orange-100 text-orange-700 rounded font-medium"> <span className="shrink-0 text-xs px-1.5 py-0.5 bg-gradient-to-r from-orange-500 to-orange-600 text-white rounded font-semibold shadow-sm">
#{task.deployRecordId} #{task.deployRecordId}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 text-xs text-muted-foreground"> <div className="flex items-center gap-2 text-xs text-gray-600">
<code className="px-1.5 py-0.5 bg-muted rounded font-mono"> <code className="px-1.5 py-0.5 bg-gray-100 text-gray-700 rounded font-mono font-medium">
{task.applicationCode} {task.applicationCode}
</code> </code>
<span></span> <span className="text-gray-400"></span>
<span>{task.environmentName}</span> <span className="font-medium">{task.environmentName}</span>
<span></span> <span className="text-gray-400"></span>
<span>{task.teamName}</span> <span className="font-medium">{task.teamName}</span>
</div> </div>
</div> </div>
@ -219,7 +223,7 @@ export const PendingApprovalModal: React.FC<PendingApprovalModalProps> = ({
<div className="flex gap-2 shrink-0"> <div className="flex gap-2 shrink-0">
<Button <Button
size="sm" size="sm"
className="h-8 bg-green-600 hover:bg-green-700 text-white shadow-sm" className="h-8 px-3 bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white shadow-md shadow-green-500/30 hover:shadow-lg hover:shadow-green-500/40 transition-all duration-200 font-semibold text-xs"
onClick={() => handleOpenApproval(task, ApprovalResult.APPROVED)} onClick={() => handleOpenApproval(task, ApprovalResult.APPROVED)}
> >
<CheckCircle className="h-3.5 w-3.5 mr-1" /> <CheckCircle className="h-3.5 w-3.5 mr-1" />
@ -228,7 +232,7 @@ export const PendingApprovalModal: React.FC<PendingApprovalModalProps> = ({
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
className="h-8 border-red-200 text-red-600 hover:bg-red-50 hover:text-red-700 hover:border-red-300" className="h-8 px-3 border-2 border-red-200 text-red-600 hover:bg-red-50 hover:text-red-700 hover:border-red-400 font-semibold transition-all duration-200 text-xs"
onClick={() => handleOpenApproval(task, ApprovalResult.REJECTED)} onClick={() => handleOpenApproval(task, ApprovalResult.REJECTED)}
> >
<XCircle className="h-3.5 w-3.5 mr-1" /> <XCircle className="h-3.5 w-3.5 mr-1" />
@ -239,14 +243,15 @@ export const PendingApprovalModal: React.FC<PendingApprovalModalProps> = ({
{/* 审批内容 */} {/* 审批内容 */}
{(task.approvalTitle || task.approvalContent) && ( {(task.approvalTitle || task.approvalContent) && (
<div className="p-2.5 bg-orange-50/60 border border-orange-100 rounded text-sm"> <div className="p-2.5 bg-gradient-to-r from-orange-50 to-amber-50 border border-orange-200/60 rounded-lg text-sm shadow-sm">
{task.approvalTitle && ( {task.approvalTitle && (
<div className="font-medium text-foreground mb-0.5"> <div className="font-semibold text-gray-900 mb-0.5 flex items-center gap-1.5 text-xs">
<div className="w-1 h-3 bg-orange-500 rounded-full" />
{task.approvalTitle} {task.approvalTitle}
</div> </div>
)} )}
{task.approvalContent && ( {task.approvalContent && (
<div className="text-xs text-muted-foreground"> <div className="text-xs text-gray-600 leading-relaxed pl-2.5">
{task.approvalContent} {task.approvalContent}
</div> </div>
)} )}
@ -255,26 +260,26 @@ export const PendingApprovalModal: React.FC<PendingApprovalModalProps> = ({
{/* 底部信息栏 */} {/* 底部信息栏 */}
<div className="flex items-center justify-between pt-1"> <div className="flex items-center justify-between pt-1">
<div className="flex items-center gap-3 text-xs text-muted-foreground"> <div className="flex items-center gap-3 text-xs">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1 text-gray-600">
<User className="h-3 w-3" /> <User className="h-3 w-3 text-blue-600" />
<span>{task.deployBy}</span> <span className="font-medium">{task.deployBy}</span>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1 text-gray-600">
<Calendar className="h-3 w-3" /> <Calendar className="h-3 w-3 text-purple-600" />
<span>{formatTime(task.deployStartTime)}</span> <span className="font-medium">{formatTime(task.deployStartTime)}</span>
</div> </div>
{task.pendingDuration && ( {task.pendingDuration && (
<div className="flex items-center gap-1 text-amber-600"> <div className="flex items-center gap-1 px-1.5 py-0.5 bg-amber-100 rounded">
<Clock className="h-3 w-3" /> <Clock className="h-3 w-3 text-amber-600" />
<span> {formatDuration(task.pendingDuration)}</span> <span className="text-amber-700 font-semibold"> {formatDuration(task.pendingDuration)}</span>
</div> </div>
)} )}
</div> </div>
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
className="h-6 text-xs opacity-0 group-hover:opacity-100 transition-opacity" className="h-6 px-2 text-xs text-orange-600 hover:text-orange-700 hover:bg-orange-50 opacity-0 group-hover:opacity-100 transition-all duration-200 font-medium"
onClick={() => handleViewDetail(task)} onClick={() => handleViewDetail(task)}
> >
@ -283,9 +288,9 @@ export const PendingApprovalModal: React.FC<PendingApprovalModalProps> = ({
{/* 备注(如果有) */} {/* 备注(如果有) */}
{task.deployRemark && ( {task.deployRemark && (
<div className="flex items-start gap-1.5 p-2 bg-muted/30 rounded text-xs"> <div className="flex items-start gap-1.5 p-2 bg-gray-50 border border-gray-200 rounded text-xs">
<FileText className="h-3 w-3 text-muted-foreground shrink-0 mt-0.5" /> <FileText className="h-3 w-3 text-gray-600 shrink-0 mt-0.5" />
<span className="text-muted-foreground">{task.deployRemark}</span> <span className="text-gray-700 leading-relaxed">{task.deployRemark}</span>
</div> </div>
)} )}
</div> </div>
@ -300,77 +305,107 @@ export const PendingApprovalModal: React.FC<PendingApprovalModalProps> = ({
{/* 审批确认对话框 */} {/* 审批确认对话框 */}
<AlertDialog open={approvalDialogOpen} onOpenChange={setApprovalDialogOpen}> <AlertDialog open={approvalDialogOpen} onOpenChange={setApprovalDialogOpen}>
<AlertDialogContent className="max-w-md"> <AlertDialogContent className="max-w-lg">
<AlertDialogHeader> <AlertDialogHeader className="space-y-3">
<AlertDialogTitle className="flex items-center gap-2"> <div className="flex items-center justify-center">
<div className={
approvalResult === ApprovalResult.APPROVED
? "w-16 h-16 rounded-full bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center shadow-lg shadow-green-500/30"
: "w-16 h-16 rounded-full bg-gradient-to-br from-red-500 to-red-600 flex items-center justify-center shadow-lg shadow-red-500/30"
}>
{approvalResult === ApprovalResult.APPROVED ? ( {approvalResult === ApprovalResult.APPROVED ? (
<> <CheckCircle className="h-8 w-8 text-white" />
<CheckCircle className="h-5 w-5 text-green-600" />
</>
) : ( ) : (
<> <XCircle className="h-8 w-8 text-white" />
<XCircle className="h-5 w-5 text-red-600" />
</>
)} )}
</div>
</div>
<AlertDialogTitle className="text-center text-xl font-bold">
{approvalResult === ApprovalResult.APPROVED ? '确认通过审批' : '确认拒绝审批'}
</AlertDialogTitle> </AlertDialogTitle>
</AlertDialogHeader> </AlertDialogHeader>
<div className="space-y-3 px-6 py-4"> <div className="space-y-4 px-6 py-2">
{selectedTask && ( {selectedTask && (
<> <>
<div className="text-sm text-muted-foreground"> <div className="text-center text-sm text-gray-600 px-4">
{approvalResult === ApprovalResult.APPROVED ? '通过' : '拒绝'}
<span className={
approvalResult === ApprovalResult.APPROVED
? "font-semibold text-green-600 mx-1"
: "font-semibold text-red-600 mx-1"
}>
{approvalResult === ApprovalResult.APPROVED ? '通过' : '拒绝'}
</span>
</div> </div>
<div className="p-3 bg-muted/50 rounded-md space-y-1.5 text-xs"> <div className="p-4 bg-gradient-to-br from-gray-50 to-gray-100/50 border border-gray-200 rounded-xl space-y-3 text-sm shadow-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
<span className="text-muted-foreground min-w-[56px]">ID</span> <span className="text-gray-500 min-w-[64px] font-medium">ID</span>
<span className="font-mono font-semibold">#{selectedTask.deployRecordId}</span> <span className="font-mono font-bold text-orange-600">#{selectedTask.deployRecordId}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="h-px bg-gray-200" />
<span className="text-muted-foreground min-w-[56px]"></span> <div className="flex items-center gap-3">
<span className="font-medium">{selectedTask.applicationName}</span> <span className="text-gray-500 min-w-[64px] font-medium"></span>
<span className="font-semibold text-gray-900">{selectedTask.applicationName}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
<span className="text-muted-foreground min-w-[56px]"></span> <span className="text-gray-500 min-w-[64px] font-medium"></span>
<span className="font-medium">{selectedTask.environmentName}</span> <span className="font-semibold text-gray-900">{selectedTask.environmentName}</span>
</div> </div>
<div className="flex items-center gap-3">
<span className="text-gray-500 min-w-[64px] font-medium"></span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-muted-foreground min-w-[56px]"></span> <div className="w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center">
<span className="font-medium">{selectedTask.deployBy}</span> <User className="h-3.5 w-3.5 text-blue-600" />
</div>
<span className="font-semibold text-gray-900">{selectedTask.deployBy}</span>
</div>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2.5">
<label className="text-sm font-medium text-foreground flex items-center gap-1"> <label className="text-sm font-semibold text-gray-900 flex items-center gap-2">
<div className="w-1 h-4 bg-orange-500 rounded-full" />
{selectedTask.requireComment && ( {selectedTask.requireComment && (
<span className="text-destructive text-xs">*</span> <span className="text-xs px-2 py-0.5 bg-red-100 text-red-700 rounded-md font-medium"></span>
)} )}
</label> </label>
<Textarea <Textarea
placeholder={selectedTask.requireComment ? "请填写审批意见(必填)" : "请填写审批意见(可选)"} placeholder={selectedTask.requireComment ? "请填写审批意见..." : "请填写审批意见(可选)..."}
value={approvalComment} value={approvalComment}
onChange={(e) => setApprovalComment(e.target.value)} onChange={(e) => setApprovalComment(e.target.value)}
rows={3} rows={4}
className="resize-none" className="resize-none border-gray-300 focus:border-orange-400 focus:ring-orange-400/20 rounded-lg"
/> />
</div> </div>
</> </>
)} )}
</div> </div>
<AlertDialogFooter> <AlertDialogFooter className="gap-3 sm:gap-3">
<AlertDialogCancel></AlertDialogCancel> <AlertDialogCancel
onClick={() => setApprovalDialogOpen(false)}
className="flex-1 h-11 border-2 border-gray-300 hover:bg-gray-50 font-semibold"
>
</AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={handleSubmitApproval} onClick={handleSubmitApproval}
disabled={submitting} disabled={submitting}
className={ className={
approvalResult === ApprovalResult.APPROVED approvalResult === ApprovalResult.APPROVED
? "bg-green-600 hover:bg-green-700" ? "flex-1 h-11 bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 text-white shadow-lg shadow-green-500/30 font-semibold"
: "bg-red-600 hover:bg-red-700" : "flex-1 h-11 bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white shadow-lg shadow-red-500/30 font-semibold"
} }
> >
{submitting ? '提交中...' : '确认'} {submitting ? (
<span className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
...
</span>
) : (
'确认'
)}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>

View File

@ -1,5 +1,11 @@
import request from '@/utils/request'; import request from '@/utils/request';
import type { DeployEnvironmentsResponse, StartDeploymentResponse, PendingApprovalTask, CompleteApprovalRequest } from './types'; import type {
DeployEnvironmentsResponse,
StartDeploymentResponse,
PendingApprovalTask,
PendingApprovalTaskResponse,
CompleteApprovalRequest
} from './types';
const DEPLOY_URL = '/api/v1/deploy'; const DEPLOY_URL = '/api/v1/deploy';
@ -23,17 +29,61 @@ export const startDeployment = (deployData: Record<string, any>) =>
export const getDeployRecordFlowGraph = (deployRecordId: number) => export const getDeployRecordFlowGraph = (deployRecordId: number) =>
request.get<import('./types').DeployRecordFlowGraph>(`${DEPLOY_URL}/records/${deployRecordId}/flow-graph`); request.get<import('./types').DeployRecordFlowGraph>(`${DEPLOY_URL}/records/${deployRecordId}/flow-graph`);
/**
*
*/
const transformApprovalTask = (response: PendingApprovalTaskResponse): PendingApprovalTask => {
return {
// 审批任务基本信息
taskId: response.approvalTask.taskId,
taskName: response.approvalTask.taskName,
taskDescription: response.approvalTask.taskDescription,
processInstanceId: response.approvalTask.processInstanceId,
processDefinitionId: response.approvalTask.processDefinitionId,
assignee: response.approvalTask.assignee || '',
createTime: response.approvalTask.createTime,
dueDate: response.approvalTask.dueDate || undefined,
approvalTitle: response.approvalTask.approvalTitle,
approvalContent: response.approvalTask.approvalContent,
approvalMode: response.approvalTask.approvalMode,
allowDelegate: response.approvalTask.allowDelegate,
allowAddSign: response.approvalTask.allowAddSign,
requireComment: response.approvalTask.requireComment,
pendingDuration: response.approvalTask.pendingDuration,
// 部署业务上下文信息
deployRecordId: response.deployRecord.id,
businessKey: response.deployRecord.businessKey,
deployRemark: response.deployRecord.remark,
deployStartTime: response.deployRecord.startTime,
teamId: response.team.id,
teamName: response.team.name,
applicationId: response.application.id,
applicationCode: response.application.code,
applicationName: response.application.name,
environmentId: response.environment.id,
environmentCode: response.environment.code,
environmentName: response.environment.name,
// 部署人信息 - 优先使用 nickname其次 username
deployBy: response.deployUser.nickname || response.deployUser.username,
};
};
/** /**
* *
* @param workflowDefinitionKeys * @param workflowDefinitionKeys
* @param teamId ID * @param teamId ID
* @param environmentId ID * @param environmentId ID
*/ */
export const getMyApprovalTasks = ( export const getMyApprovalTasks = async (
workflowDefinitionKeys?: string[], workflowDefinitionKeys?: string[],
teamId?: number, teamId?: number,
environmentId?: number environmentId?: number
) => { ): Promise<PendingApprovalTask[]> => {
const params: Record<string, any> = {}; const params: Record<string, any> = {};
if (workflowDefinitionKeys && workflowDefinitionKeys.length > 0) { if (workflowDefinitionKeys && workflowDefinitionKeys.length > 0) {
@ -48,7 +98,13 @@ export const getMyApprovalTasks = (
params.environmentId = environmentId; params.environmentId = environmentId;
} }
return request.get<PendingApprovalTask[]>(`${DEPLOY_URL}/my-approval-tasks`, { params }); const response = await request.get<PendingApprovalTaskResponse[]>(
`${DEPLOY_URL}/my-approval-tasks`,
{ params }
);
// 将嵌套结构转换为扁平结构
return response.map(transformApprovalTask);
}; };
/** /**

View File

@ -120,7 +120,57 @@ export interface StartDeploymentResponse {
} }
/** /**
* *
*/
export interface PendingApprovalTaskResponse {
approvalTask: {
taskId: string;
taskName: string;
taskDescription?: string;
processInstanceId: string;
processDefinitionId: string;
assignee: string | null;
createTime: string;
dueDate?: string | null;
pendingDuration?: number;
approvalTitle?: string;
approvalContent?: string;
approvalMode: 'SINGLE' | 'MULTI' | 'OR' | 'ANY';
allowDelegate?: boolean;
allowAddSign?: boolean;
requireComment?: boolean;
};
deployRecord: {
id: number;
businessKey: string;
remark?: string;
startTime: string;
};
team: {
id: number;
name: string;
};
application: {
id: number;
code: string;
name: string;
};
environment: {
id: number;
code: string;
name: string;
};
deployUser: {
username: string;
nickname: string;
email: string;
phone: string;
departmentName: string;
};
}
/**
* 使
*/ */
export interface PendingApprovalTask { export interface PendingApprovalTask {
// ============ 审批任务基本信息 ============ // ============ 审批任务基本信息 ============
@ -134,7 +184,7 @@ export interface PendingApprovalTask {
dueDate?: string; dueDate?: string;
approvalTitle?: string; approvalTitle?: string;
approvalContent?: string; approvalContent?: string;
approvalMode: 'SINGLE' | 'MULTI' | 'OR'; approvalMode: 'SINGLE' | 'MULTI' | 'OR' | 'ANY';
allowDelegate?: boolean; allowDelegate?: boolean;
allowAddSign?: boolean; allowAddSign?: boolean;
requireComment?: boolean; requireComment?: boolean;

View File

@ -1,4 +1,4 @@
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo, useCallback } from 'react';
import { useReactFlow } from '@xyflow/react'; import { useReactFlow } from '@xyflow/react';
import { SmartStepEdge as BaseSmartEdge } from '@tisoap/react-flow-smart-edge'; import { SmartStepEdge as BaseSmartEdge } from '@tisoap/react-flow-smart-edge';
import type { FlowNode } from '../types'; import type { FlowNode } from '../types';
@ -7,9 +7,15 @@ import { convertToDisplayName } from '@/utils/workflow/variableConversion';
/** /**
* - * -
* 使 SmartStepEdge * 使 SmartStepEdge
*
* 便
* -
* -
* -
*/ */
const CustomEdge: React.FC<any> = (props) => { const CustomEdge: React.FC<any> = (props) => {
const { const {
id,
style = {}, style = {},
markerEnd, markerEnd,
label, label,
@ -18,7 +24,7 @@ const CustomEdge: React.FC<any> = (props) => {
} = props; } = props;
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const { getNodes } = useReactFlow(); const { getNodes, setEdges } = useReactFlow();
// 将 label 中的 UUID 转换为节点名 // 将 label 中的 UUID 转换为节点名
const displayLabel = useMemo(() => { const displayLabel = useMemo(() => {
@ -61,10 +67,26 @@ const CustomEdge: React.FC<any> = (props) => {
const labelBgPadding = [4, 8] as [number, number]; const labelBgPadding = [4, 8] as [number, number];
const labelBgBorderRadius = 6; const labelBgBorderRadius = 6;
/**
*
*
*/
const handleDoubleClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
setEdges(eds => eds.map(ed => {
if (ed.id !== id) return ed;
// 清除可能存储的自定义路径数据
const { vertices, ...restData } = (ed.data || {}) as any;
return { ...ed, data: restData };
}));
}, [id, setEdges]);
return ( return (
<g <g
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)} onMouseLeave={() => setIsHovered(false)}
onDoubleClick={handleDoubleClick}
style={{ cursor: selected ? 'pointer' : 'default' }}
> >
<BaseSmartEdge <BaseSmartEdge
{...restProps} {...restProps}