This commit is contained in:
dengqichen 2025-10-22 16:46:26 +08:00
parent 541a7f821a
commit 337320bfcd
8 changed files with 128 additions and 81 deletions

View File

@ -173,7 +173,10 @@ const VariableInput: React.FC<VariableInputProps> = ({
inputElement.removeEventListener('scroll', syncScroll);
}
};
}, [value, variant]);
// ✅ 性能优化:移除 value 依赖,避免每次输入都重新同步样式
// ResizeObserver 会自动处理元素大小变化
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [variant]);
// 监听滚动和窗口大小变化,更新下拉框位置
useEffect(() => {

View File

@ -4,9 +4,18 @@ import type { NodeVariable } from '@/utils/workflow/collectNodeVariables';
/**
*
*
* @param text
* @param cursorPos
* @returns
*
* 1. ${
* 2. ${ }
* 3. 100
*
* @param text
* @param cursorPos 0
* @returns
*
* @example
* detectTrigger("Hello ${jen", 11)
* // => { shouldShow: true, startPos: 6, searchText: "jen" }
*/
export const detectTrigger = (text: string, cursorPos: number): TriggerInfo => {
// 获取光标前的文本
@ -39,12 +48,18 @@ export const detectTrigger = (text: string, cursorPos: number): TriggerInfo => {
/**
*
*
* @param originalText
* @param cursorPos
* @param triggerStartPos ${
* @param nodeName
* @param fieldName
* @returns
* ${
*
* @param originalText
* @param cursorPos
* @param triggerStartPos ${
* @param nodeName ${nodeName.fieldName}
* @param fieldName
* @returns
*
* @example
* insertVariable("Hello ${jen", 11, 6, "Jenkins构建", "buildNumber")
* // => { newText: "Hello ${Jenkins构建.buildNumber}", newCursorPos: 31 }
*/
export const insertVariable = (
originalText: string,

View File

@ -50,10 +50,12 @@ const CustomEdge: React.FC<EdgeProps> = ({
transition: 'all 0.2s ease',
};
// ✅ 修复:确保 markerEnd 是对象后再 spread
const markerEndStyle = (() => {
if (!markerEnd || typeof markerEnd !== 'object') return markerEnd;
if (selected) return { ...markerEnd, color: '#3b82f6' };
if (isHovered) return { ...markerEnd, color: '#64748b' };
const markerObj = markerEnd as Record<string, any>;
if (selected) return { ...markerObj, color: '#3b82f6' };
if (isHovered) return { ...markerObj, color: '#64748b' };
return markerEnd;
})();
@ -63,7 +65,7 @@ const CustomEdge: React.FC<EdgeProps> = ({
id={id}
path={edgePath}
style={edgeStyle}
markerEnd={markerEndStyle}
markerEnd={markerEndStyle as any}
/>
{/* 增加点击区域 */}

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Save, RotateCcw } from 'lucide-react';
@ -46,6 +46,16 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
const [dataSourceCache, setDataSourceCache] = useState<Record<string, DataSourceOption[]>>({});
const [loadingDataSources, setLoadingDataSources] = useState(false);
// ✅ 性能优化:使用 ref 保存 allNodes 和 allEdges 的最新引用
// 避免这些频繁变化的数组导致 useCallback 重建
const allNodesRef = useRef<FlowNode[]>(allNodes);
const allEdgesRef = useRef<FlowEdge[]>(allEdges);
useEffect(() => {
allNodesRef.current = allNodes;
allEdgesRef.current = allEdges;
}, [allNodes, allEdges]);
// 获取节点定义
const nodeDefinition: WorkflowNodeDefinition | null = node?.data?.nodeDefinition || null;
@ -77,24 +87,34 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
}
setLoadingDataSources(true);
try {
const cache: Record<string, DataSourceOption[]> = {};
await Promise.all(
inputTypes.map(async (type) => {
const cache: Record<string, DataSourceOption[]> = {};
let failedCount = 0;
// ✅ 改进:单独处理每个数据源的加载,避免一个失败影响全部
await Promise.allSettled(
inputTypes.map(async (type) => {
try {
const data = await loadDataSource(type as DataSourceType);
cache[type] = data;
})
);
setDataSourceCache(cache);
} catch (error) {
console.error('加载动态数据源失败:', error);
} catch (error) {
console.error(`加载数据源 ${type} 失败:`, error);
failedCount++;
cache[type] = []; // 失败时使用空数组
}
})
);
setDataSourceCache(cache);
setLoadingDataSources(false);
// 如果有失败的数据源,显示提示
if (failedCount > 0) {
toast({
title: '数据加载失败',
description: '无法加载部分选项数据,请刷新重试',
title: '部分数据加载失败',
description: `${failedCount} 个选项数据加载失败,请检查网络后重新打开节点配置`,
variant: 'destructive'
});
} finally {
setLoadingDataSources(false);
}
};
@ -212,59 +232,58 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
});
};
// ✅ 通用 Select 渲染函数(减少代码重复)
const renderSelect = useCallback((
options: Array<{ label: string; value: any }>,
field: any,
prop: any,
disabled: boolean,
onValueChange?: (value: string) => void
) => (
<Select
disabled={disabled}
value={field.value?.toString()}
onValueChange={onValueChange || field.onChange}
>
<SelectTrigger>
<SelectValue placeholder={`请选择${prop.title || ''}`} />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={option.value.toString()}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
), []);
// ✅ 渲染字段控件
const renderFieldControl = useCallback((prop: any, field: any, key: string) => {
// 动态数据源
if (prop['x-dataSource']) {
const options = dataSourceCache[prop['x-dataSource']] || [];
return (
<Select
disabled={loadingDataSources || loading}
value={field.value?.toString()}
onValueChange={(value) => {
// 如果是数字类型,转换为数字
const numValue = Number(value);
field.onChange(isNaN(numValue) ? value : numValue);
}}
>
<SelectTrigger>
<SelectValue placeholder={`请选择${prop.title || ''}`} />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={option.value.toString()}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
return renderSelect(
options,
field,
prop,
loadingDataSources || loading,
(value) => {
// 如果是数字类型,转换为数字
const numValue = Number(value);
field.onChange(isNaN(numValue) ? value : numValue);
}
);
}
// 静态枚举值
if (prop.enum && Array.isArray(prop.enum)) {
return (
<Select
disabled={loading}
value={field.value?.toString()}
onValueChange={field.onChange}
>
<SelectTrigger>
<SelectValue placeholder={`请选择${prop.title || ''}`} />
</SelectTrigger>
<SelectContent>
{prop.enum.map((value: any, index: number) => {
const enumValue = typeof value === 'object' ? value.value : value;
const enumLabel = typeof value === 'object' ? value.label : (prop.enumNames?.[index] || value);
return (
<SelectItem key={enumValue} value={enumValue.toString()}>
{enumLabel}
</SelectItem>
);
})}
</SelectContent>
</Select>
);
const options = prop.enum.map((value: any, index: number) => {
const enumValue = typeof value === 'object' ? value.value : value;
const enumLabel = typeof value === 'object' ? value.label : (prop.enumNames?.[index] || value);
return { label: enumLabel, value: enumValue };
});
return renderSelect(options, field, prop, loading);
}
// 根据类型渲染
@ -305,8 +324,8 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
<VariableInput
value={field.value || ''}
onChange={field.onChange}
allNodes={allNodes}
allEdges={allEdges}
allNodes={allNodesRef.current}
allEdges={allEdgesRef.current}
currentNodeId={node?.id || ''}
variant={isTextarea ? 'textarea' : 'input'}
placeholder={prop.description || `请输入${prop.title || ''}`}
@ -351,7 +370,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
/>
);
}
}, [dataSourceCache, loadingDataSources, loading, node, allNodes, allEdges]);
}, [dataSourceCache, loadingDataSources, loading, node, renderSelect]);
// ✅ 渲染表单字段
const renderFormFields = useCallback((
@ -437,7 +456,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
<p className="text-xs text-muted-foreground">
</p>
</div>
</div>
{/* 节点名称 */}
<div className="space-y-2">
@ -453,7 +472,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
<p className="text-xs text-muted-foreground">
</p>
</div>
</div>
{/* 节点描述 */}
<div className="space-y-2">
@ -470,7 +489,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
<p className="text-xs text-muted-foreground">
</p>
</div>
</div>
</AccordionContent>
</AccordionItem>

View File

@ -39,7 +39,6 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
},
// 基本配置Schema仅业务配置元数据已移至固定表单
configSchema: undefined,
inputMappingSchema: {
type: "object",
title: "输入",

View File

@ -39,7 +39,6 @@ export const NotificationNodeDefinition: ConfigurableNodeDefinition = {
},
// 基本配置Schema仅业务配置元数据已移至固定表单
configSchema: undefined,
inputMappingSchema: {
type: "object",
title: "输入",
@ -60,7 +59,7 @@ export const NotificationNodeDefinition: ConfigurableNodeDefinition = {
// 'x-dataSource': DataSourceType.NOTIFICATION_CHANNEL_TYPES
// },
notificationChannel: {
type: "string",
type: "number",
title: "通知渠道",
description: "选择通知发送的渠道",
'x-dataSource': DataSourceType.NOTIFICATION_CHANNELS

View File

@ -141,7 +141,6 @@ export interface BaseNodeDefinition {
category: NodeCategory;
description: string;
renderConfig: RenderConfig; // ✨ 渲染配置(配置驱动)
configSchema: JSONSchema; // 基本配置Schema包含基本信息+节点配置)
}
// 可配置节点定义有3个面板基本配置、输入映射、输出能力

View File

@ -16,10 +16,21 @@ export interface NodeVariable {
/**
* DFS反向遍历
*
* @param currentNodeId ID
*
* 1. incoming edges
* 2.
* 3. 使 visited
* 4. START_EVENT
*
* @param currentNodeId IDUUID
* @param allNodes
* @param allEdges
* @returns START节点
*
* @example
* // 工作流:开始 -> Jenkins -> 通知 -> 结束
* getAllPrecedingNodes("通知节点ID", allNodes, allEdges)
* // => [Jenkins节点] (不包含开始节点)
*/
export const getAllPrecedingNodes = (
currentNodeId: string,