重构前端逻辑

This commit is contained in:
dengqichen 2025-11-07 17:02:44 +08:00
parent ccc8bc591e
commit 3b77cf7539
9 changed files with 440 additions and 437 deletions

View File

@ -50,7 +50,6 @@ interface CustomNodeData {
endTime?: string | null; endTime?: string | null;
duration?: number | null; duration?: number | null;
errorMessage?: string | null; errorMessage?: string | null;
isUnreachable?: boolean;
processInstanceId?: string; processInstanceId?: string;
onViewLog?: (nodeId: string, nodeName: string) => void; onViewLog?: (nodeId: string, nodeName: string) => void;
} }
@ -59,7 +58,7 @@ interface CustomNodeData {
* *
*/ */
const CustomFlowNode: React.FC<any> = ({ data }) => { const CustomFlowNode: React.FC<any> = ({ data }) => {
const { nodeName, nodeType, status, startTime, endTime, duration, errorMessage, isUnreachable } = data as CustomNodeData; const { nodeName, nodeType, status, startTime, endTime, duration, errorMessage } = data as CustomNodeData;
const statusColor = getNodeStatusColor(status); const statusColor = getNodeStatusColor(status);
const isNotStarted = status === 'NOT_STARTED'; const isNotStarted = status === 'NOT_STARTED';
const isRunning = status === 'RUNNING'; const isRunning = status === 'RUNNING';
@ -89,7 +88,6 @@ const CustomFlowNode: React.FC<any> = ({ data }) => {
isNotStarted && 'border-2 border-dashed', isNotStarted && 'border-2 border-dashed',
!isNotStarted && 'border-2 border-solid shadow-sm', !isNotStarted && 'border-2 border-solid shadow-sm',
isRunning && 'animate-pulse', isRunning && 'animate-pulse',
isUnreachable && 'opacity-40', // 不可达节点半透明
canViewLog && 'cursor-pointer hover:shadow-md hover:scale-[1.02]' // 可查看日志的节点增加交互效果 canViewLog && 'cursor-pointer hover:shadow-md hover:scale-[1.02]' // 可查看日志的节点增加交互效果
)} )}
style={{ style={{
@ -461,123 +459,95 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
return map; return map;
}, [flowData]); }, [flowData]);
// 计算可达节点(从已执行节点出发能到达的节点) // 智能过滤:只显示实际执行路径 + 到终点的路径
const reachableNodes = useMemo(() => { const visibleNodeIds = useMemo(() => {
if (!flowData?.graph?.edges || !flowData?.nodeInstances) return new Set<string>(); if (!flowData?.graph?.nodes || !flowData?.graph?.edges || !flowData?.nodeInstances) {
return new Set<string>();
}
const executedNodeIds = flowData.nodeInstances const visible = new Set<string>();
.filter(ni => ni.status !== 'NOT_STARTED')
.map(ni => ni.nodeId); // 1. 收集所有已执行的节点id !== null 表示后端创建了实例)
const executedNodes = flowData.nodeInstances.filter(ni => ni.id !== null);
executedNodes.forEach(ni => visible.add(ni.nodeId));
if (executedNodeIds.length === 0) return new Set<string>(); // 如果没有执行任何节点,显示所有节点(初始状态)
if (executedNodes.length === 0) {
flowData.graph.nodes.forEach(n => visible.add(n.id));
return visible;
}
const reachable = new Set<string>(executedNodeIds); // 2. 找出最后执行的节点(按时间排序)
const queue = [...executedNodeIds]; const lastExecutedNode = [...executedNodes].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 timeB - timeA; // 降序
})[0];
// BFS 遍历所有可达节点 // 3. 找出所有终点节点(没有出边的节点,通常是 END_EVENT
while (queue.length > 0) { const allNodeIds = new Set(flowData.graph.nodes.map(n => n.id));
const currentId = queue.shift()!; const nodesWithOutgoingEdges = new Set(flowData.graph.edges.map(e => e.from));
const outgoingEdges = flowData.graph.edges.filter(e => e.from === currentId); const endNodes = flowData.graph.nodes.filter(n => !nodesWithOutgoingEdges.has(n.id));
for (const edge of outgoingEdges) { // 4. BFS 从最后执行的节点到所有终点节点的路径
if (!reachable.has(edge.to)) { if (lastExecutedNode && endNodes.length > 0) {
reachable.add(edge.to); const queue = [lastExecutedNode.nodeId];
queue.push(edge.to); const visited = new Set<string>([lastExecutedNode.nodeId]);
while (queue.length > 0) {
const currentId = queue.shift()!;
const outgoingEdges = flowData.graph.edges.filter(e => e.from === currentId);
for (const edge of outgoingEdges) {
if (!visited.has(edge.to)) {
visited.add(edge.to);
visible.add(edge.to); // 添加到可见集合
queue.push(edge.to);
}
} }
} }
} }
return reachable; return visible;
}, [flowData]); }, [flowData]);
// 转换为 React Flow 节点(显示所有节点,但对不可达节点置灰) // 转换为 React Flow 节点(只显示可见节点
const flowNodes: Node[] = useMemo(() => { const flowNodes: Node[] = useMemo(() => {
if (!flowData?.graph?.nodes || !flowData?.nodeInstances) return []; if (!flowData?.graph?.nodes || !flowData?.nodeInstances) return [];
const isRunning = flowData.runningNodeCount > 0; // 过滤并转换为 React Flow 节点
return flowData.graph.nodes
.filter(node => visibleNodeIds.has(node.id)) // 只显示可见节点
.map((node) => {
const instance = nodeInstanceMap.get(node.id);
// 按执行顺序排序已执行的节点 return {
const executedInstances = flowData.nodeInstances id: node.id,
.filter(ni => ni.status !== 'NOT_STARTED') type: 'custom',
.sort((a, b) => { position: node.position || { x: 0, y: 0 }, // 使用设计器中保存的坐标
const timeA = a.startTime ? new Date(a.startTime.replace(' ', 'T')).getTime() : 0; data: {
const timeB = b.startTime ? new Date(b.startTime.replace(' ', 'T')).getTime() : 0; nodeName: node.nodeName,
return timeA - timeB; nodeType: instance?.nodeType || node.nodeType, // 优先使用运行时的 nodeType
nodeId: node.id,
status: instance?.status || 'NOT_STARTED',
startTime: instance?.startTime,
endTime: instance?.endTime,
duration: instance?.duration,
errorMessage: instance?.errorMessage,
processInstanceId: flowData.processInstanceId,
},
};
}); });
}, [flowData, nodeInstanceMap, visibleNodeIds]);
// 线性布局:从左到右排列 // 转换为 React Flow 边(只显示连接可见节点的边)
const nodePositionMap = new Map<string, { x: number; y: number }>();
const horizontalSpacing = 300; // 水平间距
const verticalSpacing = 150; // 垂直间距(用于分支)
const startX = 100; // 起始X坐标
const startY = 150; // 起始Y坐标
// 已执行的节点:从左到右线性排列
executedInstances.forEach((instance, index) => {
nodePositionMap.set(instance.nodeId, {
x: startX + index * horizontalSpacing,
y: startY,
});
});
// 未执行的节点:根据在图中的位置和层级布局
const notStartedNodes = flowData.graph.nodes.filter(
node => !nodePositionMap.has(node.id)
);
// 简单布局按节点在edges中的出现顺序从左到右、上到下排列
let currentX = startX + executedInstances.length * horizontalSpacing;
let currentRow = 0;
const nodesPerRow = 3;
notStartedNodes.forEach((node, index) => {
const row = Math.floor(index / nodesPerRow);
const col = index % nodesPerRow;
nodePositionMap.set(node.id, {
x: currentX + col * horizontalSpacing,
y: startY + row * verticalSpacing,
});
});
// 生成所有节点
return flowData.graph.nodes.map((node) => {
const instance = nodeInstanceMap.get(node.id);
const position = nodePositionMap.get(node.id) || { x: 0, y: 0 };
const isReachable = reachableNodes.has(node.id);
const isNotStarted = !instance || instance.status === 'NOT_STARTED';
return {
id: node.id,
type: 'custom',
position,
data: {
nodeName: node.nodeName,
nodeType: node.nodeType,
nodeId: node.id,
status: instance?.status || 'NOT_STARTED',
startTime: instance?.startTime,
endTime: instance?.endTime,
duration: instance?.duration,
errorMessage: instance?.errorMessage,
// 新增:不可达且未执行的节点标记为置灰
isUnreachable: isRunning && isNotStarted && !isReachable,
processInstanceId: flowData.processInstanceId,
},
};
});
}, [flowData, nodeInstanceMap, reachableNodes]);
// 转换为 React Flow 边
const flowEdges: Edge[] = useMemo(() => { const flowEdges: Edge[] = useMemo(() => {
if (!flowData?.graph?.edges) return []; if (!flowData?.graph?.edges) return [];
const isRunning = flowData.runningNodeCount > 0;
const displayedNodeIds = new Set(flowNodes.map(n => n.id));
return flowData.graph.edges return flowData.graph.edges
.filter(edge => { .filter(edge => {
// 只显示连接已显示节点的边 // 只显示连接可见节点的边
return displayedNodeIds.has(edge.from) && displayedNodeIds.has(edge.to); return visibleNodeIds.has(edge.from) && visibleNodeIds.has(edge.to);
}) })
.map((edge, index) => { .map((edge, index) => {
const source = edge.from; const source = edge.from;
@ -587,20 +557,21 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
const sourceStatus = sourceInstance?.status || 'NOT_STARTED'; const sourceStatus = sourceInstance?.status || 'NOT_STARTED';
const targetStatus = targetInstance?.status || 'NOT_STARTED'; const targetStatus = targetInstance?.status || 'NOT_STARTED';
// 判断这条边是否在可达路径上
const isReachableEdge = reachableNodes.has(source) && reachableNodes.has(target);
// 根据节点状态确定边的样式 // 根据节点状态确定边的样式
let strokeColor = '#d1d5db'; // 默认灰色 let strokeColor = '#d1d5db'; // 默认灰色
let strokeWidth = 2; let strokeWidth = 2;
let animated = false; let animated = false;
let opacity = 1; let strokeDasharray: string | undefined = undefined;
// 源节点已完成 + 目标节点也已完成/运行中 = 绿色实线 // 源节点已完成 + 目标节点也已完成/运行中 = 绿色实线
if (sourceStatus === 'COMPLETED' && (targetStatus === 'COMPLETED' || targetStatus === 'RUNNING')) { if (sourceStatus === 'COMPLETED' && (targetStatus === 'COMPLETED' || targetStatus === 'RUNNING')) {
strokeColor = '#10b981'; // 绿色 strokeColor = '#10b981'; // 绿色
} }
// 源节点失败 = 红色 // 源节点 TERMINATED = 橙色实线
else if (sourceStatus === 'TERMINATED') {
strokeColor = '#f59e0b'; // 橙色
}
// 源节点失败 = 红色实线
else if (sourceStatus === 'FAILED') { else if (sourceStatus === 'FAILED') {
strokeColor = '#ef4444'; // 红色 strokeColor = '#ef4444'; // 红色
} }
@ -609,31 +580,22 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
strokeColor = '#3b82f6'; // 蓝色 strokeColor = '#3b82f6'; // 蓝色
animated = true; animated = true;
} }
// 源节点完成 + 目标节点未开始 // 源节点完成 + 目标节点未开始 = 虚线(即将执行的路径)
else if (sourceStatus === 'COMPLETED' && targetStatus === 'NOT_STARTED') { else if ((sourceStatus === 'COMPLETED' || sourceStatus === 'TERMINATED') && targetStatus === 'NOT_STARTED') {
if (isReachableEdge && isRunning) { strokeColor = '#9ca3af'; // 浅灰色
strokeColor = '#9ca3af'; // 浅灰色(即将执行) strokeDasharray = '5,5';
} else {
strokeColor = '#d1d5db'; // 默认灰色
opacity = 0.3; // 不可达路径半透明
}
}
// 两端都未执行
else if (sourceStatus === 'NOT_STARTED' && targetStatus === 'NOT_STARTED') {
opacity = isRunning && !isReachableEdge ? 0.3 : 0.5;
} }
return { return {
id: edge.id || `edge-${source}-${target}-${index}`, id: edge.id || `edge-${source}-${target}-${index}`,
source, source,
target, target,
type: 'smoothstep', type: 'straight', // 使用直线类型
animated, animated,
style: { style: {
stroke: strokeColor, stroke: strokeColor,
strokeWidth, strokeWidth,
opacity, strokeDasharray,
strokeDasharray: (sourceStatus === 'COMPLETED' && targetStatus === 'NOT_STARTED' && isReachableEdge) ? '5,5' : undefined,
}, },
markerEnd: { markerEnd: {
type: 'arrowclosed', type: 'arrowclosed',
@ -643,7 +605,7 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
}, },
}; };
}); });
}, [flowData, nodeInstanceMap, flowNodes, reachableNodes]); }, [flowData, nodeInstanceMap, visibleNodeIds]);
// 获取部署状态信息 // 获取部署状态信息
const deployStatusInfo = flowData const deployStatusInfo = flowData

View File

@ -151,35 +151,49 @@ const DeployNodeLogDialog: React.FC<DeployNodeLogDialogProps> = ({
<ScrollArea className="flex-1 border rounded-md bg-gray-50" ref={scrollAreaRef}> <ScrollArea className="flex-1 border rounded-md bg-gray-50" ref={scrollAreaRef}>
<div className="p-2 font-mono text-xs"> <div className="p-2 font-mono text-xs">
{logData?.logs && logData.logs.length > 0 ? ( {logData?.logs && logData.logs.length > 0 ? (
logData.logs.map((log, index) => ( logData.logs.map((log, index) => {
<div // 计算行号宽度
key={log.sequenceId} const lineNumWidth = Math.max(4, String(logData.logs.length).length + 1);
className="flex items-start hover:bg-gray-200 px-2 py-0.5 whitespace-nowrap"
> // 格式化时间戳为可读格式2025-11-07 16:27:41.494
{/* 行号 - 根据总行数动态调整宽度 */} const timestamp = dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS');
<span
className="text-muted-foreground flex-shrink-0 text-right pr-3 select-none" return (
style={{ width: `${Math.max(3, String(logData.logs.length).length)}ch` }} <div
key={log.sequenceId}
className="flex items-start hover:bg-gray-100 px-2 py-0.5 whitespace-nowrap"
> >
{index + 1} {/* 行号 - 动态宽度,右对齐 */}
</span> <span
{/* 时间 - 18个字符宽度 */} className="text-gray-400 flex-shrink-0 text-right select-none"
<span className="text-muted-foreground flex-shrink-0 pr-2" style={{ width: '18ch' }}> style={{ width: `${lineNumWidth}ch`, marginRight: '1ch' }}
{dayjs(log.timestamp).format('MM-DD HH:mm:ss.SSS')} >
</span> {index + 1}
{/* 级别 - 5个字符宽度 */} </span>
<span
className={cn('flex-shrink-0 font-semibold pr-2', getLevelClass(log.level))} {/* 时间戳 - 可读格式23个字符 (2025-11-07 16:27:41.494) */}
style={{ width: '5ch' }} <span
> className="text-gray-600 flex-shrink-0"
{log.level} style={{ width: '23ch', marginRight: '2ch' }}
</span> >
{/* 日志内容 - 不换行,支持水平滚动 */} {timestamp}
<span className="flex-1 text-gray-800 whitespace-nowrap overflow-x-auto"> </span>
{log.message}
</span> {/* 日志级别 - 5个字符右对齐 */}
</div> <span
)) className={cn('flex-shrink-0 font-semibold text-right', getLevelClass(log.level))}
style={{ width: '5ch', marginRight: '2ch' }}
>
{log.level}
</span>
{/* 日志消息 - 占据剩余空间,不换行 */}
<span className="flex-1 text-gray-800 whitespace-nowrap overflow-x-auto">
{log.message}
</span>
</div>
);
})
) : ( ) : (
<div className="flex flex-col items-center justify-center h-64 text-muted-foreground"> <div className="flex flex-col items-center justify-center h-64 text-muted-foreground">
<Clock className="h-12 w-12 mb-4 text-muted-foreground/30" /> <Clock className="h-12 w-12 mb-4 text-muted-foreground/30" />

View File

@ -214,7 +214,7 @@ const ApplicationList: React.FC = () => {
{ {
accessorKey: 'appCode', accessorKey: 'appCode',
header: '应用编码', header: '应用编码',
size: 150, size: 200,
}, },
{ {
accessorKey: 'appName', accessorKey: 'appName',
@ -249,7 +249,7 @@ const ApplicationList: React.FC = () => {
{ {
id: 'repository', id: 'repository',
header: '代码仓库', header: '代码仓库',
size: 200, size: 280,
cell: ({ row }) => { cell: ({ row }) => {
const project = row.original.repositoryProject; const project = row.original.repositoryProject;
return project ? ( return project ? (

View File

@ -39,6 +39,7 @@ interface TeamApplicationDialogProps {
applications: Application[]; // 可选择的应用列表 applications: Application[]; // 可选择的应用列表
jenkinsSystems: any[]; jenkinsSystems: any[];
workflowDefinitions: WorkflowDefinition[]; workflowDefinitions: WorkflowDefinition[];
existingApplicationIds?: number[]; // 已添加的应用ID列表用于过滤
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
onSave: (data: { onSave: (data: {
id?: number; id?: number;
@ -62,6 +63,7 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
applications, applications,
jenkinsSystems, jenkinsSystems,
workflowDefinitions, workflowDefinitions,
existingApplicationIds = [],
onOpenChange, onOpenChange,
onSave, onSave,
onLoadBranches, onLoadBranches,
@ -209,11 +211,7 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
}); });
onOpenChange(false); onOpenChange(false);
} catch (error: any) { } catch (error: any) {
toast({ // 错误已经在 request.ts 中通过 toast 显示了,这里不需要再显示
variant: 'destructive',
title: mode === 'edit' ? '保存失败' : '添加失败',
description: error.response?.data?.message || error.message,
});
} finally { } finally {
setSaving(false); setSaving(false);
} }
@ -254,17 +252,24 @@ const TeamApplicationDialog: React.FC<TeamApplicationDialogProps> = ({
<SelectValue placeholder="选择应用" /> <SelectValue placeholder="选择应用" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{applications.length === 0 ? ( {(() => {
<div className="p-4 text-center text-sm text-muted-foreground"> // 在新建模式下,过滤掉已添加的应用
const availableApps = mode === 'create'
</div> ? applications.filter(app => !existingApplicationIds.includes(app.id))
) : ( : applications;
applications.map((app) => (
<SelectItem key={app.id} value={app.id.toString()}> return availableApps.length === 0 ? (
{app.appName}{app.appCode} <div className="p-4 text-center text-sm text-muted-foreground">
</SelectItem> {mode === 'create' ? '暂无可添加的应用' : '暂无应用'}
)) </div>
)} ) : (
availableApps.map((app) => (
<SelectItem key={app.id} value={app.id.toString()}>
{app.appName}{app.appCode}
</SelectItem>
))
);
})()}
</SelectContent> </SelectContent>
</Select> </Select>
{mode === 'edit' && ( {mode === 'edit' && (

View File

@ -246,7 +246,9 @@ export const TeamApplicationManageDialog: React.FC<
{teamApplications.map((app) => ( {teamApplications.map((app) => (
<TableRow key={app.id}> <TableRow key={app.id}>
<TableCell className="font-medium"> <TableCell className="font-medium">
{app.applicationName || `应用 ${app.applicationId}`} {app.applicationName && app.applicationCode
? `${app.applicationName}${app.applicationCode}`
: app.applicationName || app.applicationCode || `应用 ${app.applicationId}`}
</TableCell> </TableCell>
<TableCell> <TableCell>
{getEnvironmentName(app.environmentId)} {getEnvironmentName(app.environmentId)}
@ -257,10 +259,7 @@ export const TeamApplicationManageDialog: React.FC<
</TableCell> </TableCell>
<TableCell>{app.deployJob || '-'}</TableCell> <TableCell>{app.deployJob || '-'}</TableCell>
<TableCell> <TableCell>
{app.workflowDefinitionName || {app.workflowDefinitionName || '-'}
(app.workflowDefinitionId
? `工作流 ${app.workflowDefinitionId}`
: '-')}
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-2">
@ -309,6 +308,12 @@ export const TeamApplicationManageDialog: React.FC<
applications={applications} applications={applications}
jenkinsSystems={jenkinsSystems} jenkinsSystems={jenkinsSystems}
workflowDefinitions={workflowDefinitions} workflowDefinitions={workflowDefinitions}
existingApplicationIds={
// 传递当前环境已添加的应用ID列表
teamApplications
.filter(app => app.environmentId === editingEnvironment.id)
.map(app => app.applicationId)
}
onSave={handleSaveApplication} onSave={handleSaveApplication}
onLoadBranches={handleLoadBranches} onLoadBranches={handleLoadBranches}
onLoadJenkinsJobs={handleLoadJenkinsJobs} onLoadJenkinsJobs={handleLoadJenkinsJobs}
@ -320,7 +325,11 @@ export const TeamApplicationManageDialog: React.FC<
open={deleteDialogOpen} open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen} onOpenChange={setDeleteDialogOpen}
title="确认删除" title="确认删除"
description={`确定要删除应用配置 "${deletingApp?.applicationName || `应用 ${deletingApp?.applicationId}`}" 吗?此操作无法撤销。`} description={`确定要删除应用配置 "${
deletingApp?.applicationName && deletingApp?.applicationCode
? `${deletingApp.applicationName}${deletingApp.applicationCode}`
: deletingApp?.applicationName || deletingApp?.applicationCode || `应用 ${deletingApp?.applicationId}`
}" `}
item={deletingApp} item={deletingApp}
onConfirm={handleConfirmDelete} onConfirm={handleConfirmDelete}
onSuccess={() => { onSuccess={() => {

View File

@ -63,7 +63,7 @@ const formSchema = z.object({
environmentId: z.number().min(1, '请选择环境'), environmentId: z.number().min(1, '请选择环境'),
approvalRequired: z.boolean().default(false), approvalRequired: z.boolean().default(false),
approverUserIds: z.array(z.number()).default([]), approverUserIds: z.array(z.number()).default([]),
notificationChannelId: z.number().optional(), notificationChannelId: z.number().nullish(),
notificationEnabled: z.boolean().default(false), notificationEnabled: z.boolean().default(false),
requireCodeReview: z.boolean().default(false), requireCodeReview: z.boolean().default(false),
remark: z.string().max(100, '备注最多100个字符').optional(), remark: z.string().max(100, '备注最多100个字符').optional(),
@ -253,6 +253,7 @@ export const TeamEnvironmentConfigDialog: React.FC<
}; };
const handleSubmit = async (data: FormData) => { const handleSubmit = async (data: FormData) => {
console.log('表单提交开始', data);
setSubmitting(true); setSubmitting(true);
try { try {
const payload = { const payload = {
@ -266,6 +267,8 @@ export const TeamEnvironmentConfigDialog: React.FC<
remark: data.remark, remark: data.remark,
}; };
console.log('提交数据', payload);
if (configId) { if (configId) {
// 更新已有配置 // 更新已有配置
await updateTeamEnvironmentConfig(configId, payload); await updateTeamEnvironmentConfig(configId, payload);
@ -285,6 +288,7 @@ export const TeamEnvironmentConfigDialog: React.FC<
onOpenChange(false); onOpenChange(false);
onSuccess?.(); onSuccess?.();
} catch (error: any) { } catch (error: any) {
console.error('保存失败', error);
toast({ toast({
title: '保存失败', title: '保存失败',
description: error.message || '保存环境配置失败', description: error.message || '保存环境配置失败',
@ -295,6 +299,13 @@ export const TeamEnvironmentConfigDialog: React.FC<
} }
}; };
const handleSaveClick = () => {
console.log('保存按钮被点击');
console.log('表单值', form.getValues());
console.log('表单错误', form.formState.errors);
form.handleSubmit(handleSubmit)();
};
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl">
@ -304,222 +315,37 @@ export const TeamEnvironmentConfigDialog: React.FC<
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<DialogBody> {loading ? (
{loading ? ( <DialogBody>
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div> </div>
) : ( </DialogBody>
<Form {...form}> ) : (
<form <Form {...form}>
onSubmit={form.handleSubmit(handleSubmit)} <DialogBody>
className="space-y-6" <div className="space-y-6">
> {/* 环境选择 */}
{/* 环境选择 */}
<FormField
control={form.control}
name="environmentId"
render={({ field }) => (
<FormItem>
<FormLabel> *</FormLabel>
<Select
value={field.value?.toString() || ''}
onValueChange={(value) => field.onChange(Number(value))}
disabled={!!initialData || !!editEnvironmentId}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="请选择环境" />
</SelectTrigger>
</FormControl>
<SelectContent>
{environments.map((env) => (
<SelectItem key={env.id} value={env.id.toString()}>
{env.envName}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{/* 是否需要审批 */}
<FormField
control={form.control}
name="approvalRequired"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base"></FormLabel>
<div className="text-sm text-muted-foreground">
</div>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{/* 审批人多选 - 仅在需要审批时显示 */}
{form.watch('approvalRequired') && (
<FormField <FormField
control={form.control} control={form.control}
name="approverUserIds" name="environmentId"
render={({ field }) => {
const [open, setOpen] = useState(false);
const selectedUsers = (users || []).filter(u => field.value?.includes(u.id));
return (
<FormItem className="flex flex-col">
<FormLabel> *</FormLabel>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between"
>
{selectedUsers.length > 0 ? (
<div className="flex gap-1 flex-wrap">
{selectedUsers.map((user) => (
<Badge
key={user.id}
variant="secondary"
className="mr-1"
>
{user.realName || user.username}
<button
type="button"
className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
onKeyDown={(e) => {
if (e.key === "Enter") {
field.onChange(field.value?.filter(id => id !== user.id) || []);
}
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={() => {
field.onChange(field.value?.filter(id => id !== user.id) || []);
}}
>
<X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
</button>
</Badge>
))}
</div>
) : (
"请选择审批人"
)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput placeholder="搜索审批人..." />
<CommandList>
<CommandEmpty></CommandEmpty>
<CommandGroup>
{(users || []).map((user) => {
const isSelected = field.value?.includes(user.id);
return (
<CommandItem
key={user.id}
value={`${user.id}-${user.username}-${user.realName || ''}`}
onSelect={() => {
const newValue = isSelected
? field.value?.filter(id => id !== user.id) || []
: [...(field.value || []), user.id];
field.onChange(newValue);
}}
>
<Check
className={`mr-2 h-4 w-4 ${
isSelected ? "opacity-100" : "opacity-0"
}`}
/>
{user.realName || user.username}
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
);
}}
/>
)}
{/* 启用通知 */}
<FormField
control={form.control}
name="notificationEnabled"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base"></FormLabel>
<div className="text-sm text-muted-foreground">
</div>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{/* 通知渠道选择 - 仅在启用通知时显示 */}
{form.watch('notificationEnabled') && (
<FormField
control={form.control}
name="notificationChannelId"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> *</FormLabel> <FormLabel> *</FormLabel>
<Select <Select
value={field.value?.toString() || ''} value={field.value?.toString() || ''}
onValueChange={(value) => onValueChange={(value) => field.onChange(Number(value))}
field.onChange(value ? Number(value) : undefined) disabled={!!initialData || !!editEnvironmentId}
}
disabled={loadingChannels}
> >
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue <SelectValue placeholder="请选择环境" />
placeholder={
loadingChannels
? '加载中...'
: '请选择通知渠道'
}
/>
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
{notificationChannels.map((channel) => ( {environments.map((env) => (
<SelectItem <SelectItem key={env.id} value={env.id.toString()}>
key={channel.id} {env.envName}
value={channel.id.toString()}
>
{channel.name} ({channel.type})
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>
@ -528,72 +354,259 @@ export const TeamEnvironmentConfigDialog: React.FC<
</FormItem> </FormItem>
)} )}
/> />
)}
{/* 需要代码审查 */} {/* 是否需要审批 */}
<FormField <FormField
control={form.control} control={form.control}
name="requireCodeReview" name="approvalRequired"
render={({ field }) => ( render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4"> <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5"> <div className="space-y-0.5">
<FormLabel className="text-base"></FormLabel> <FormLabel className="text-base"></FormLabel>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
</div>
</div> </div>
</div> <FormControl>
<FormControl> <Switch
<Switch checked={field.value}
checked={field.value} onCheckedChange={field.onChange}
onCheckedChange={field.onChange} />
/> </FormControl>
</FormControl> </FormItem>
</FormItem> )}
)} />
/>
{/* 备注 */} {/* 审批人多选 - 仅在需要审批时显示 */}
<FormField {form.watch('approvalRequired') && (
control={form.control} <FormField
name="remark" control={form.control}
render={({ field }) => ( name="approverUserIds"
<FormItem> render={({ field }) => {
<FormLabel></FormLabel> const [open, setOpen] = useState(false);
<FormControl> const selectedUsers = (users || []).filter(u => field.value?.includes(u.id));
<Textarea
placeholder="请输入备注信息最多100字符" return (
className="resize-none" <FormItem className="flex flex-col">
rows={3} <FormLabel> *</FormLabel>
{...field} <Popover open={open} onOpenChange={setOpen}>
/> <PopoverTrigger asChild>
</FormControl> <FormControl>
<FormMessage /> <Button
</FormItem> type="button"
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between"
>
{selectedUsers.length > 0 ? (
<div className="flex gap-1 flex-wrap">
{selectedUsers.map((user) => (
<Badge
key={user.id}
variant="secondary"
className="mr-1"
>
{user.realName || user.username}
<button
type="button"
className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
onKeyDown={(e) => {
if (e.key === "Enter") {
field.onChange(field.value?.filter(id => id !== user.id) || []);
}
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={(e) => {
e.stopPropagation();
field.onChange(field.value?.filter(id => id !== user.id) || []);
}}
>
<X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
</button>
</Badge>
))}
</div>
) : (
"请选择审批人"
)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput placeholder="搜索审批人..." />
<CommandList>
<CommandEmpty></CommandEmpty>
<CommandGroup>
{(users || []).map((user) => {
const isSelected = field.value?.includes(user.id);
return (
<CommandItem
key={user.id}
value={`${user.id}-${user.username}-${user.realName || ''}`}
onSelect={() => {
const newValue = isSelected
? field.value?.filter(id => id !== user.id) || []
: [...(field.value || []), user.id];
field.onChange(newValue);
}}
>
<Check
className={`mr-2 h-4 w-4 ${
isSelected ? "opacity-100" : "opacity-0"
}`}
/>
{user.realName || user.username}
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
);
}}
/>
)} )}
/>
</form> {/* 启用通知 */}
<FormField
control={form.control}
name="notificationEnabled"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base"></FormLabel>
<div className="text-sm text-muted-foreground">
</div>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{/* 通知渠道选择 - 仅在启用通知时显示 */}
{form.watch('notificationEnabled') && (
<FormField
control={form.control}
name="notificationChannelId"
render={({ field }) => (
<FormItem>
<FormLabel> *</FormLabel>
<Select
value={field.value?.toString() || ''}
onValueChange={(value) =>
field.onChange(value ? Number(value) : undefined)
}
disabled={loadingChannels}
>
<FormControl>
<SelectTrigger>
<SelectValue
placeholder={
loadingChannels
? '加载中...'
: '请选择通知渠道'
}
/>
</SelectTrigger>
</FormControl>
<SelectContent>
{notificationChannels.map((channel) => (
<SelectItem
key={channel.id}
value={channel.id.toString()}
>
{channel.name} ({channel.type})
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
{/* 需要代码审查 */}
<FormField
control={form.control}
name="requireCodeReview"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base"></FormLabel>
<div className="text-sm text-muted-foreground">
</div>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{/* 备注 */}
<FormField
control={form.control}
name="remark"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Textarea
placeholder="请输入备注信息最多100字符"
className="resize-none"
rows={3}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</DialogBody>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={submitting}
>
</Button>
<Button
type="button"
onClick={handleSaveClick}
disabled={submitting || loading}
>
{submitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
</Button>
</DialogFooter>
</Form> </Form>
)} )}
</DialogBody>
<DialogFooter>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={submitting}
>
</Button>
<Button
onClick={form.handleSubmit(handleSubmit)}
disabled={submitting}
>
{submitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
</Button>
</DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );
}; };

View File

@ -38,7 +38,7 @@ const FormDataDetail: React.FC = () => {
// 返回列表 // 返回列表
const handleBack = () => { const handleBack = () => {
navigate('/workflow/form/data'); navigate(-1); // 返回上一页,避免硬编码路径
}; };
// 状态徽章 // 状态徽章

View File

@ -81,7 +81,7 @@ const FormDesignerPage: React.FC = () => {
// 返回列表 // 返回列表
const handleBack = () => { const handleBack = () => {
navigate('/workflow/form'); navigate(-1); // 返回上一页,避免硬编码路径
}; };
return ( return (

View File

@ -180,7 +180,7 @@ const WorkflowDesignInner: React.FC = () => {
}, [getNodes, getEdges, saveWorkflow, currentWorkflowId, workflowTitle, workflowDefinition]); }, [getNodes, getEdges, saveWorkflow, currentWorkflowId, workflowTitle, workflowDefinition]);
const handleBack = useCallback(() => { const handleBack = useCallback(() => {
navigate('/workflow/definition'); navigate(-1); // 返回上一页,避免硬编码路径
}, [navigate]); }, [navigate]);
// 预览表单 // 预览表单