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

View File

@ -4,9 +4,18 @@ import type { NodeVariable } from '@/utils/workflow/collectNodeVariables';
/** /**
* *
* *
* @param text *
* @param cursorPos * 1. ${
* @returns * 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 => { 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 originalText
* @param nodeName * @param cursorPos
* @param fieldName * @param triggerStartPos ${
* @returns * @param nodeName ${nodeName.fieldName}
* @param fieldName
* @returns
*
* @example
* insertVariable("Hello ${jen", 11, 6, "Jenkins构建", "buildNumber")
* // => { newText: "Hello ${Jenkins构建.buildNumber}", newCursorPos: 31 }
*/ */
export const insertVariable = ( export const insertVariable = (
originalText: string, originalText: string,

View File

@ -50,10 +50,12 @@ const CustomEdge: React.FC<EdgeProps> = ({
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
}; };
// ✅ 修复:确保 markerEnd 是对象后再 spread
const markerEndStyle = (() => { const markerEndStyle = (() => {
if (!markerEnd || typeof markerEnd !== 'object') return markerEnd; if (!markerEnd || typeof markerEnd !== 'object') return markerEnd;
if (selected) return { ...markerEnd, color: '#3b82f6' }; const markerObj = markerEnd as Record<string, any>;
if (isHovered) return { ...markerEnd, color: '#64748b' }; if (selected) return { ...markerObj, color: '#3b82f6' };
if (isHovered) return { ...markerObj, color: '#64748b' };
return markerEnd; return markerEnd;
})(); })();
@ -63,7 +65,7 @@ const CustomEdge: React.FC<EdgeProps> = ({
id={id} id={id}
path={edgePath} path={edgePath}
style={edgeStyle} 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 { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Save, RotateCcw } from 'lucide-react'; import { Save, RotateCcw } from 'lucide-react';
@ -46,6 +46,16 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
const [dataSourceCache, setDataSourceCache] = useState<Record<string, DataSourceOption[]>>({}); const [dataSourceCache, setDataSourceCache] = useState<Record<string, DataSourceOption[]>>({});
const [loadingDataSources, setLoadingDataSources] = useState(false); 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; const nodeDefinition: WorkflowNodeDefinition | null = node?.data?.nodeDefinition || null;
@ -77,24 +87,34 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
} }
setLoadingDataSources(true); setLoadingDataSources(true);
try {
const cache: Record<string, DataSourceOption[]> = {}; const cache: Record<string, DataSourceOption[]> = {};
await Promise.all( let failedCount = 0;
// ✅ 改进:单独处理每个数据源的加载,避免一个失败影响全部
await Promise.allSettled(
inputTypes.map(async (type) => { inputTypes.map(async (type) => {
try {
const data = await loadDataSource(type as DataSourceType); const data = await loadDataSource(type as DataSourceType);
cache[type] = data; cache[type] = data;
} catch (error) {
console.error(`加载数据源 ${type} 失败:`, error);
failedCount++;
cache[type] = []; // 失败时使用空数组
}
}) })
); );
setDataSourceCache(cache); setDataSourceCache(cache);
} catch (error) { setLoadingDataSources(false);
console.error('加载动态数据源失败:', error);
// 如果有失败的数据源,显示提示
if (failedCount > 0) {
toast({ toast({
title: '数据加载失败', title: '部分数据加载失败',
description: '无法加载部分选项数据,请刷新重试', description: `${failedCount} 个选项数据加载失败,请检查网络后重新打开节点配置`,
variant: 'destructive' variant: 'destructive'
}); });
} finally {
setLoadingDataSources(false);
} }
}; };
@ -212,20 +232,18 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
}); });
}; };
// ✅ 渲染字段控件 // ✅ 通用 Select 渲染函数(减少代码重复)
const renderFieldControl = useCallback((prop: any, field: any, key: string) => { const renderSelect = useCallback((
// 动态数据源 options: Array<{ label: string; value: any }>,
if (prop['x-dataSource']) { field: any,
const options = dataSourceCache[prop['x-dataSource']] || []; prop: any,
return ( disabled: boolean,
onValueChange?: (value: string) => void
) => (
<Select <Select
disabled={loadingDataSources || loading} disabled={disabled}
value={field.value?.toString()} value={field.value?.toString()}
onValueChange={(value) => { onValueChange={onValueChange || field.onChange}
// 如果是数字类型,转换为数字
const numValue = Number(value);
field.onChange(isNaN(numValue) ? value : numValue);
}}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder={`请选择${prop.title || ''}`} /> <SelectValue placeholder={`请选择${prop.title || ''}`} />
@ -238,33 +256,34 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
), []);
// ✅ 渲染字段控件
const renderFieldControl = useCallback((prop: any, field: any, key: string) => {
// 动态数据源
if (prop['x-dataSource']) {
const options = dataSourceCache[prop['x-dataSource']] || [];
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)) { if (prop.enum && Array.isArray(prop.enum)) {
return ( const options = prop.enum.map((value: any, index: number) => {
<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 enumValue = typeof value === 'object' ? value.value : value;
const enumLabel = typeof value === 'object' ? value.label : (prop.enumNames?.[index] || value); const enumLabel = typeof value === 'object' ? value.label : (prop.enumNames?.[index] || value);
return ( return { label: enumLabel, value: enumValue };
<SelectItem key={enumValue} value={enumValue.toString()}> });
{enumLabel} return renderSelect(options, field, prop, loading);
</SelectItem>
);
})}
</SelectContent>
</Select>
);
} }
// 根据类型渲染 // 根据类型渲染
@ -305,8 +324,8 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
<VariableInput <VariableInput
value={field.value || ''} value={field.value || ''}
onChange={field.onChange} onChange={field.onChange}
allNodes={allNodes} allNodes={allNodesRef.current}
allEdges={allEdges} allEdges={allEdgesRef.current}
currentNodeId={node?.id || ''} currentNodeId={node?.id || ''}
variant={isTextarea ? 'textarea' : 'input'} variant={isTextarea ? 'textarea' : 'input'}
placeholder={prop.description || `请输入${prop.title || ''}`} 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(( const renderFormFields = useCallback((

View File

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

View File

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

View File

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

View File

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