1
This commit is contained in:
parent
56338dc3a8
commit
313307a19d
256
frontend/src/components/VariableInput/index.tsx
Normal file
256
frontend/src/components/VariableInput/index.tsx
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
import React, { useState, useRef, useMemo, useEffect } from 'react';
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/components/ui/command';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import type { VariableInputProps } from './types';
|
||||||
|
import { detectTrigger, insertVariable, filterVariables } from './utils';
|
||||||
|
import { collectNodeVariables, groupVariablesByNode } from '@/utils/workflow/collectNodeVariables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 高亮渲染 ${} 变量
|
||||||
|
*/
|
||||||
|
const highlightVariables = (text: string): React.ReactNode[] => {
|
||||||
|
const regex = /\$\{([^}]+)\}/g;
|
||||||
|
const parts: React.ReactNode[] = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
// 添加变量前的文本
|
||||||
|
if (match.index > lastIndex) {
|
||||||
|
parts.push(text.substring(lastIndex, match.index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加高亮的变量
|
||||||
|
parts.push(
|
||||||
|
<span
|
||||||
|
key={match.index}
|
||||||
|
className="inline-block px-1.5 py-0.5 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded font-mono text-sm border border-blue-200 dark:border-blue-800"
|
||||||
|
>
|
||||||
|
${`{${match[1]}}`}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
lastIndex = match.index + match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加剩余文本
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
parts.push(text.substring(lastIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.length > 0 ? parts : [text];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变量输入组件
|
||||||
|
* 支持在输入 ${ 时自动提示可用变量
|
||||||
|
*/
|
||||||
|
const VariableInput: React.FC<VariableInputProps> = ({
|
||||||
|
value = '',
|
||||||
|
onChange,
|
||||||
|
allNodes,
|
||||||
|
allEdges,
|
||||||
|
currentNodeId,
|
||||||
|
variant = 'input',
|
||||||
|
placeholder,
|
||||||
|
disabled = false,
|
||||||
|
rows = 3,
|
||||||
|
}) => {
|
||||||
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||||
|
const [triggerInfo, setTriggerInfo] = useState({ shouldShow: false, startPos: -1, searchText: '' });
|
||||||
|
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 点击外部关闭建议框
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showSuggestions) return;
|
||||||
|
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||||
|
setShowSuggestions(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, [showSuggestions]);
|
||||||
|
|
||||||
|
// 收集可用变量
|
||||||
|
const allVariables = useMemo(() => {
|
||||||
|
return collectNodeVariables(currentNodeId, allNodes, allEdges);
|
||||||
|
}, [currentNodeId, allNodes, allEdges]);
|
||||||
|
|
||||||
|
// 根据搜索文本过滤变量
|
||||||
|
const filteredVariables = useMemo(() => {
|
||||||
|
return filterVariables(allVariables, triggerInfo.searchText);
|
||||||
|
}, [allVariables, triggerInfo.searchText]);
|
||||||
|
|
||||||
|
// 按节点分组
|
||||||
|
const groupedVariables = useMemo(() => {
|
||||||
|
return groupVariablesByNode(filteredVariables);
|
||||||
|
}, [filteredVariables]);
|
||||||
|
|
||||||
|
// 处理输入变化
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
const newValue = e.target.value;
|
||||||
|
const cursorPos = e.target.selectionStart || 0;
|
||||||
|
|
||||||
|
onChange(newValue);
|
||||||
|
|
||||||
|
// 检测是否应该触发变量建议
|
||||||
|
const trigger = detectTrigger(newValue, cursorPos);
|
||||||
|
setTriggerInfo(trigger);
|
||||||
|
setShowSuggestions(trigger.shouldShow && !disabled);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理键盘事件
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
// Esc 键关闭建议框
|
||||||
|
if (e.key === 'Escape' && showSuggestions) {
|
||||||
|
setShowSuggestions(false);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 插入变量
|
||||||
|
const handleSelectVariable = (nodeName: string, fieldName: string) => {
|
||||||
|
console.log('🎯 handleSelectVariable 被调用:', { nodeName, fieldName, value, triggerInfo });
|
||||||
|
|
||||||
|
if (!inputRef.current) {
|
||||||
|
console.error('❌ inputRef.current 不存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cursorPos = inputRef.current.selectionStart || 0;
|
||||||
|
console.log('📍 当前光标位置:', cursorPos);
|
||||||
|
|
||||||
|
// 使用工具函数插入变量
|
||||||
|
const { newText, newCursorPos } = insertVariable(
|
||||||
|
value,
|
||||||
|
cursorPos,
|
||||||
|
triggerInfo.startPos,
|
||||||
|
nodeName,
|
||||||
|
fieldName
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('✅ 插入结果:', { newText, newCursorPos });
|
||||||
|
|
||||||
|
// 更新值
|
||||||
|
onChange(newText);
|
||||||
|
|
||||||
|
// 关闭建议框
|
||||||
|
setShowSuggestions(false);
|
||||||
|
|
||||||
|
// 恢复焦点并设置光标位置
|
||||||
|
setTimeout(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
inputRef.current.setSelectionRange(newCursorPos, newCursorPos);
|
||||||
|
console.log('✅ 焦点已恢复,光标已设置');
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染输入框
|
||||||
|
const renderInput = () => {
|
||||||
|
const commonProps = {
|
||||||
|
ref: inputRef as any,
|
||||||
|
value,
|
||||||
|
onChange: handleChange,
|
||||||
|
onKeyDown: handleKeyDown,
|
||||||
|
placeholder,
|
||||||
|
disabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (variant === 'input') {
|
||||||
|
return <Input {...commonProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Textarea {...commonProps} rows={rows} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染变量提示
|
||||||
|
const renderSuggestions = () => {
|
||||||
|
if (allVariables.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
||||||
|
当前没有可用变量(没有前序节点或前序节点无输出)
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredVariables.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
||||||
|
未找到匹配的变量
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-h-80 overflow-y-auto">
|
||||||
|
{Array.from(groupedVariables.entries()).map(([nodeName, variables]) => (
|
||||||
|
<div key={nodeName} className="py-2">
|
||||||
|
<div className="px-3 py-1 text-xs font-semibold text-muted-foreground">
|
||||||
|
📦 {nodeName}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{variables.map((variable) => (
|
||||||
|
<div
|
||||||
|
key={`${variable.nodeId}.${variable.fieldName}`}
|
||||||
|
className="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-accent rounded-sm transition-colors"
|
||||||
|
onClick={() => {
|
||||||
|
console.log('点击变量:', variable.nodeName, variable.fieldName);
|
||||||
|
handleSelectVariable(variable.nodeName, variable.fieldName);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="font-mono text-sm">{variable.fieldName}</span>
|
||||||
|
<span className="ml-auto text-xs text-muted-foreground">{variable.fieldType}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检测是否有变量需要高亮
|
||||||
|
const hasVariables = value && /\$\{[^}]+\}/.test(value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className="relative space-y-2">
|
||||||
|
{/* 输入框 */}
|
||||||
|
{renderInput()}
|
||||||
|
|
||||||
|
{/* 变量高亮预览 */}
|
||||||
|
{hasVariables && !showSuggestions && (
|
||||||
|
<div className="p-2 bg-muted/30 rounded-md border border-border text-sm">
|
||||||
|
<div className="text-xs text-muted-foreground mb-1">变量预览:</div>
|
||||||
|
<div className="break-words leading-relaxed">
|
||||||
|
{highlightVariables(value)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 变量提示弹窗 - 绝对定位 */}
|
||||||
|
{showSuggestions && (
|
||||||
|
<div
|
||||||
|
className="absolute top-full left-0 w-full mt-2 z-50"
|
||||||
|
>
|
||||||
|
<div className="w-full max-w-[500px] bg-popover text-popover-foreground rounded-md border shadow-lg">
|
||||||
|
{renderSuggestions()}
|
||||||
|
<div className="px-3 py-2 border-t text-xs text-muted-foreground bg-muted/30">
|
||||||
|
💡 输入 <code className="px-1 py-0.5 bg-background rounded">${'{'}</code> 触发变量提示,
|
||||||
|
按 <kbd className="px-1 py-0.5 bg-background rounded border">Esc</kbd> 关闭
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VariableInput;
|
||||||
|
|
||||||
48
frontend/src/components/VariableInput/types.ts
Normal file
48
frontend/src/components/VariableInput/types.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import type { FlowNode, FlowEdge } from '@/pages/Workflow/Design/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变量输入组件的 Props
|
||||||
|
*/
|
||||||
|
export interface VariableInputProps {
|
||||||
|
/** 输入值(显示名称格式)*/
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
/** 值变化回调 */
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
|
||||||
|
/** 所有节点(用于收集变量)*/
|
||||||
|
allNodes: FlowNode[];
|
||||||
|
|
||||||
|
/** 所有连接边(用于确定前序节点)*/
|
||||||
|
allEdges: FlowEdge[];
|
||||||
|
|
||||||
|
/** 当前节点ID(用于过滤前序节点)*/
|
||||||
|
currentNodeId: string;
|
||||||
|
|
||||||
|
/** 渲染类型 */
|
||||||
|
variant?: 'input' | 'textarea';
|
||||||
|
|
||||||
|
/** 占位符 */
|
||||||
|
placeholder?: string;
|
||||||
|
|
||||||
|
/** 是否禁用 */
|
||||||
|
disabled?: boolean;
|
||||||
|
|
||||||
|
/** Textarea 行数 */
|
||||||
|
rows?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发信息
|
||||||
|
*/
|
||||||
|
export interface TriggerInfo {
|
||||||
|
/** 是否应该显示变量提示 */
|
||||||
|
shouldShow: boolean;
|
||||||
|
|
||||||
|
/** ${ 的起始位置 */
|
||||||
|
startPos: number;
|
||||||
|
|
||||||
|
/** 搜索文本(${ 之后的文本)*/
|
||||||
|
searchText: string;
|
||||||
|
}
|
||||||
|
|
||||||
95
frontend/src/components/VariableInput/utils.ts
Normal file
95
frontend/src/components/VariableInput/utils.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import type { TriggerInfo } from './types';
|
||||||
|
import type { NodeVariable } from '@/utils/workflow/collectNodeVariables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测是否应该触发变量建议
|
||||||
|
*
|
||||||
|
* @param text 完整文本
|
||||||
|
* @param cursorPos 光标位置
|
||||||
|
* @returns 触发信息
|
||||||
|
*/
|
||||||
|
export const detectTrigger = (text: string, cursorPos: number): TriggerInfo => {
|
||||||
|
// 获取光标前的文本
|
||||||
|
const beforeCursor = text.substring(0, cursorPos);
|
||||||
|
|
||||||
|
// 找到最后一个 ${
|
||||||
|
const lastOpenIndex = beforeCursor.lastIndexOf('${');
|
||||||
|
|
||||||
|
// 如果没有找到 ${,或者找到的位置在很早之前(超过100个字符),不触发
|
||||||
|
if (lastOpenIndex === -1 || (cursorPos - lastOpenIndex) > 100) {
|
||||||
|
return { shouldShow: false, startPos: -1, searchText: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 ${ 之后到光标的文本
|
||||||
|
const textAfterOpen = beforeCursor.substring(lastOpenIndex + 2);
|
||||||
|
|
||||||
|
// 如果这段文本中已经有 },说明表达式已经闭合,不触发
|
||||||
|
if (textAfterOpen.includes('}')) {
|
||||||
|
return { shouldShow: false, startPos: -1, searchText: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回触发信息
|
||||||
|
return {
|
||||||
|
shouldShow: true,
|
||||||
|
startPos: lastOpenIndex,
|
||||||
|
searchText: textAfterOpen.trim(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入变量到文本中
|
||||||
|
*
|
||||||
|
* @param originalText 原始文本
|
||||||
|
* @param cursorPos 光标位置
|
||||||
|
* @param triggerStartPos ${ 的起始位置
|
||||||
|
* @param nodeName 节点名称
|
||||||
|
* @param fieldName 字段名
|
||||||
|
* @returns 新的文本和光标位置
|
||||||
|
*/
|
||||||
|
export const insertVariable = (
|
||||||
|
originalText: string,
|
||||||
|
cursorPos: number,
|
||||||
|
triggerStartPos: number,
|
||||||
|
nodeName: string,
|
||||||
|
fieldName: string
|
||||||
|
): { newText: string; newCursorPos: number } => {
|
||||||
|
// ${ 之前的文本
|
||||||
|
const before = originalText.substring(0, triggerStartPos);
|
||||||
|
|
||||||
|
// 光标之后的文本
|
||||||
|
const after = originalText.substring(cursorPos);
|
||||||
|
|
||||||
|
// 构建新文本:before + ${nodeName.fieldName} + after
|
||||||
|
const variableText = `\${${nodeName}.${fieldName}}`;
|
||||||
|
const newText = `${before}${variableText}${after}`;
|
||||||
|
|
||||||
|
// 计算新的光标位置(在 } 之后)
|
||||||
|
const newCursorPos = before.length + variableText.length;
|
||||||
|
|
||||||
|
return { newText, newCursorPos };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤变量列表
|
||||||
|
*
|
||||||
|
* @param variables 所有变量
|
||||||
|
* @param searchText 搜索文本
|
||||||
|
* @returns 过滤后的变量列表
|
||||||
|
*/
|
||||||
|
export const filterVariables = (variables: NodeVariable[], searchText: string): NodeVariable[] => {
|
||||||
|
if (!searchText) {
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerSearch = searchText.toLowerCase();
|
||||||
|
|
||||||
|
return variables.filter(v => {
|
||||||
|
// 匹配节点名称或字段名
|
||||||
|
const matchesNodeName = v.nodeName?.toLowerCase().includes(lowerSearch);
|
||||||
|
const matchesFieldName = v.fieldName?.toLowerCase().includes(lowerSearch);
|
||||||
|
const matchesFullText = v.fullText?.toLowerCase().includes(lowerSearch);
|
||||||
|
|
||||||
|
return matchesNodeName || matchesFieldName || matchesFullText;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
@ -6,14 +6,17 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import type { FlowEdge } from '../types';
|
import type { FlowEdge, FlowNode } from '../types';
|
||||||
|
import { convertToUUID, convertToDisplayName } from '@/utils/workflow/variableConversion';
|
||||||
|
import VariableInput from '@/components/VariableInput';
|
||||||
|
|
||||||
interface EdgeConfigModalProps {
|
interface EdgeConfigModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
edge: FlowEdge | null;
|
edge: FlowEdge | null;
|
||||||
|
allNodes: FlowNode[];
|
||||||
|
allEdges: FlowEdge[];
|
||||||
onOk: (edgeId: string, condition: EdgeCondition) => void;
|
onOk: (edgeId: string, condition: EdgeCondition) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
@ -53,6 +56,8 @@ type EdgeConditionFormValues = z.infer<typeof edgeConditionSchema>;
|
|||||||
const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
||||||
visible,
|
visible,
|
||||||
edge,
|
edge,
|
||||||
|
allNodes,
|
||||||
|
allEdges,
|
||||||
onOk,
|
onOk,
|
||||||
onCancel
|
onCancel
|
||||||
}) => {
|
}) => {
|
||||||
@ -72,22 +77,36 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && edge) {
|
if (visible && edge) {
|
||||||
const condition = edge.data?.condition;
|
const condition = edge.data?.condition;
|
||||||
|
|
||||||
|
// 转换 expression 中的 UUID 为显示名称
|
||||||
|
const displayExpression = condition?.expression
|
||||||
|
? convertToDisplayName(condition.expression, allNodes)
|
||||||
|
: '';
|
||||||
|
|
||||||
const values = {
|
const values = {
|
||||||
type: (condition?.type || 'EXPRESSION') as 'EXPRESSION' | 'DEFAULT',
|
type: (condition?.type || 'EXPRESSION') as 'EXPRESSION' | 'DEFAULT',
|
||||||
expression: condition?.expression || '',
|
expression: displayExpression,
|
||||||
priority: condition?.priority || 10,
|
priority: condition?.priority || 10,
|
||||||
};
|
};
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
setConditionType(values.type);
|
setConditionType(values.type);
|
||||||
}
|
}
|
||||||
}, [visible, edge, form]);
|
// 只在 visible 或 edge.id 改变时重置表单
|
||||||
|
// allNodes 改变不应该触发重置(用户正在编辑时)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [visible, edge?.id]);
|
||||||
|
|
||||||
const handleSubmit = (values: EdgeConditionFormValues) => {
|
const handleSubmit = (values: EdgeConditionFormValues) => {
|
||||||
if (!edge) return;
|
if (!edge) return;
|
||||||
|
|
||||||
|
// 转换 expression 中的显示名称为 UUID
|
||||||
|
const uuidExpression = values.expression
|
||||||
|
? convertToUUID(values.expression, allNodes)
|
||||||
|
: '';
|
||||||
|
|
||||||
// 检查表达式是否包含变量引用
|
// 检查表达式是否包含变量引用
|
||||||
if (values.type === 'EXPRESSION' && values.expression) {
|
if (values.type === 'EXPRESSION' && uuidExpression) {
|
||||||
const hasVariable = /\$\{[\w.]+\}/.test(values.expression);
|
const hasVariable = /\$\{[\w.-]+\}/.test(uuidExpression);
|
||||||
if (!hasVariable) {
|
if (!hasVariable) {
|
||||||
toast({
|
toast({
|
||||||
title: '提示',
|
title: '提示',
|
||||||
@ -96,7 +115,11 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onOk(edge.id, values);
|
// 提交 UUID 格式的数据
|
||||||
|
onOk(edge.id, {
|
||||||
|
...values,
|
||||||
|
expression: uuidExpression
|
||||||
|
});
|
||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -173,14 +196,19 @@ const EdgeConfigModal: React.FC<EdgeConfigModalProps> = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>条件表达式</FormLabel>
|
<FormLabel>条件表达式</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<VariableInput
|
||||||
placeholder="请输入条件表达式,如:${amount} > 1000"
|
value={field.value || ''}
|
||||||
rows={4}
|
onChange={field.onChange}
|
||||||
{...field}
|
allNodes={allNodes}
|
||||||
|
allEdges={allEdges}
|
||||||
|
currentNodeId={edge?.source || ''}
|
||||||
|
variant="textarea"
|
||||||
|
placeholder="请输入条件表达式,如:${Jenkins构建.buildStatus} == 'SUCCESS'"
|
||||||
|
rows={4}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
支持使用 {'${变量名}'} 引用流程变量,例如:{'${amount} > 1000'}
|
支持使用 {'${节点名称.字段名}'} 引用前序节点的输出变量,输入 ${'{'} 触发变量提示
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@ -10,15 +10,19 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { useToast } from '@/components/ui/use-toast';
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
import type { FlowNode, FlowNodeData } from '../types';
|
import type { FlowNode, FlowNodeData, FlowEdge } from '../types';
|
||||||
import type { WorkflowNodeDefinition, JSONSchema } from '../nodes/types';
|
import type { WorkflowNodeDefinition, JSONSchema } from '../nodes/types';
|
||||||
import { isConfigurableNode } from '../nodes/types';
|
import { isConfigurableNode } from '../nodes/types';
|
||||||
import { convertJsonSchemaToZod, extractDataSourceTypes } from '../utils/schemaConverter';
|
import { convertJsonSchemaToZod, extractDataSourceTypes } from '../utils/schemaConverter';
|
||||||
import { loadDataSource, DataSourceType, type DataSourceOption } from '../utils/dataSourceLoader';
|
import { loadDataSource, DataSourceType, type DataSourceOption } from '../utils/dataSourceLoader';
|
||||||
|
import { convertObjectToUUID, convertObjectToDisplayName } from '@/utils/workflow/variableConversion';
|
||||||
|
import VariableInput from '@/components/VariableInput';
|
||||||
|
|
||||||
interface NodeConfigModalProps {
|
interface NodeConfigModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
node: FlowNode | null;
|
node: FlowNode | null;
|
||||||
|
allNodes: FlowNode[];
|
||||||
|
allEdges: FlowEdge[];
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onOk: (nodeId: string, updatedData: Partial<FlowNodeData>) => void;
|
onOk: (nodeId: string, updatedData: Partial<FlowNodeData>) => void;
|
||||||
}
|
}
|
||||||
@ -26,6 +30,8 @@ interface NodeConfigModalProps {
|
|||||||
const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||||
visible,
|
visible,
|
||||||
node,
|
node,
|
||||||
|
allNodes,
|
||||||
|
allEdges,
|
||||||
onCancel,
|
onCancel,
|
||||||
onOk
|
onOk
|
||||||
}) => {
|
}) => {
|
||||||
@ -116,14 +122,27 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
|||||||
nodeCode: nodeDefinition.nodeCode,
|
nodeCode: nodeDefinition.nodeCode,
|
||||||
description: nodeDefinition.description || ''
|
description: nodeDefinition.description || ''
|
||||||
};
|
};
|
||||||
configForm.reset({ ...defaultConfig, ...(nodeData.configs || {}) });
|
|
||||||
|
|
||||||
// 设置输入映射默认值
|
// 转换 UUID 格式为显示名称格式
|
||||||
|
const displayConfigs = convertObjectToDisplayName(
|
||||||
|
{ ...defaultConfig, ...(nodeData.configs || {}) },
|
||||||
|
allNodes
|
||||||
|
);
|
||||||
|
configForm.reset(displayConfigs);
|
||||||
|
|
||||||
|
// 设置输入映射默认值(也需要转换)
|
||||||
if (isConfigurableNode(nodeDefinition)) {
|
if (isConfigurableNode(nodeDefinition)) {
|
||||||
inputForm.reset(nodeData.inputMapping || {});
|
const displayInputMapping = convertObjectToDisplayName(
|
||||||
|
nodeData.inputMapping || {},
|
||||||
|
allNodes
|
||||||
|
);
|
||||||
|
inputForm.reset(displayInputMapping);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [visible, node, nodeDefinition, configForm, inputForm]);
|
// 只在 visible 或 node.id 改变时重置表单
|
||||||
|
// allNodes 改变不应该触发重置(用户正在编辑时)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [visible, node?.id]);
|
||||||
|
|
||||||
// ✅ 保存配置
|
// ✅ 保存配置
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
@ -139,11 +158,15 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
|||||||
inputData = inputForm.getValues();
|
inputData = inputForm.getValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 转换显示名称格式为 UUID 格式
|
||||||
|
const uuidConfigs = convertObjectToUUID(configData as Record<string, any>, allNodes);
|
||||||
|
const uuidInputMapping = convertObjectToUUID(inputData as Record<string, any>, allNodes);
|
||||||
|
|
||||||
// 构建更新数据
|
// 构建更新数据
|
||||||
const updatedData: Partial<FlowNodeData> = {
|
const updatedData: Partial<FlowNodeData> = {
|
||||||
label: (configData as any).nodeName || nodeDefinition.nodeName,
|
label: (configData as any).nodeName || nodeDefinition.nodeName,
|
||||||
configs: configData as Record<string, any>,
|
configs: uuidConfigs,
|
||||||
inputMapping: inputData,
|
inputMapping: uuidInputMapping,
|
||||||
outputs: isConfigurableNode(nodeDefinition) ? nodeDefinition.outputs || [] : []
|
outputs: isConfigurableNode(nodeDefinition) ? nodeDefinition.outputs || [] : []
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -244,25 +267,52 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
|||||||
|
|
||||||
// 根据类型渲染
|
// 根据类型渲染
|
||||||
switch (prop.type) {
|
switch (prop.type) {
|
||||||
case 'string':
|
case 'string': {
|
||||||
if (prop.format === 'textarea' || key.includes('description')) {
|
// 黑名单字段:不使用变量输入
|
||||||
|
const blacklist = ['nodeName', 'nodeCode', 'description'];
|
||||||
|
const isBlacklisted = blacklist.includes(key);
|
||||||
|
const isPassword = prop.format === 'password';
|
||||||
|
|
||||||
|
// 密码字段或黑名单字段:使用普通输入
|
||||||
|
if (isPassword || isBlacklisted) {
|
||||||
|
if (prop.format === 'textarea' || key.includes('description')) {
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
{...field}
|
||||||
|
disabled={loading}
|
||||||
|
placeholder={prop.description || `请输入${prop.title || ''}`}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type={isPassword ? 'password' : 'text'}
|
||||||
|
disabled={loading}
|
||||||
|
placeholder={prop.description || `请输入${prop.title || ''}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他字符串字段:使用 VariableInput
|
||||||
|
const isTextarea = prop.format === 'textarea' ||
|
||||||
|
['content', 'body', 'expression', 'script'].includes(key);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Textarea
|
<VariableInput
|
||||||
{...field}
|
value={field.value || ''}
|
||||||
disabled={loading}
|
onChange={field.onChange}
|
||||||
|
allNodes={allNodes}
|
||||||
|
allEdges={allEdges}
|
||||||
|
currentNodeId={node?.id || ''}
|
||||||
|
variant={isTextarea ? 'textarea' : 'input'}
|
||||||
placeholder={prop.description || `请输入${prop.title || ''}`}
|
placeholder={prop.description || `请输入${prop.title || ''}`}
|
||||||
|
disabled={loading}
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
type={prop.format === 'password' ? 'password' : 'text'}
|
|
||||||
disabled={loading}
|
|
||||||
placeholder={prop.description || `请输入${prop.title || ''}`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'number':
|
case 'number':
|
||||||
case 'integer':
|
case 'integer':
|
||||||
@ -299,7 +349,7 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [dataSourceCache, loadingDataSources, loading]);
|
}, [dataSourceCache, loadingDataSources, loading, node, allNodes, allEdges]);
|
||||||
|
|
||||||
// ✅ 渲染表单字段
|
// ✅ 渲染表单字段
|
||||||
const renderFormFields = useCallback((
|
const renderFormFields = useCallback((
|
||||||
|
|||||||
@ -525,6 +525,8 @@ const WorkflowDesignInner: React.FC = () => {
|
|||||||
<NodeConfigModal
|
<NodeConfigModal
|
||||||
visible={configModalVisible}
|
visible={configModalVisible}
|
||||||
node={configNode}
|
node={configNode}
|
||||||
|
allNodes={getNodes() as FlowNode[]}
|
||||||
|
allEdges={getEdges() as FlowEdge[]}
|
||||||
onCancel={handleCloseConfigModal}
|
onCancel={handleCloseConfigModal}
|
||||||
onOk={handleNodeConfigUpdate}
|
onOk={handleNodeConfigUpdate}
|
||||||
/>
|
/>
|
||||||
@ -533,6 +535,8 @@ const WorkflowDesignInner: React.FC = () => {
|
|||||||
<EdgeConfigModal
|
<EdgeConfigModal
|
||||||
visible={edgeConfigModalVisible}
|
visible={edgeConfigModalVisible}
|
||||||
edge={configEdge}
|
edge={configEdge}
|
||||||
|
allNodes={getNodes() as FlowNode[]}
|
||||||
|
allEdges={getEdges() as FlowEdge[]}
|
||||||
onOk={handleEdgeConditionUpdate}
|
onOk={handleEdgeConditionUpdate}
|
||||||
onCancel={handleCloseEdgeConfigModal}
|
onCancel={handleCloseEdgeConfigModal}
|
||||||
/>
|
/>
|
||||||
|
|||||||
120
frontend/src/utils/workflow/collectNodeVariables.ts
Normal file
120
frontend/src/utils/workflow/collectNodeVariables.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import type { FlowNode, FlowEdge } from '@/pages/Workflow/Design/types';
|
||||||
|
import { isConfigurableNode } from '@/pages/Workflow/Design/nodes/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节点变量定义
|
||||||
|
*/
|
||||||
|
export interface NodeVariable {
|
||||||
|
nodeId: string; // 节点UUID
|
||||||
|
nodeName: string; // 节点显示名称
|
||||||
|
fieldName: string; // 字段名
|
||||||
|
fieldType: string; // 字段类型
|
||||||
|
displayText: string; // 界面显示格式: ${节点名称.字段名}
|
||||||
|
fullText: string; // 完整文本格式: 节点名称.字段名(用于搜索)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前节点的所有前序节点(通过DFS反向遍历)
|
||||||
|
*
|
||||||
|
* @param currentNodeId 当前节点ID
|
||||||
|
* @param allNodes 所有节点列表
|
||||||
|
* @param allEdges 所有连接边列表
|
||||||
|
* @returns 所有前序节点(不包括START节点)
|
||||||
|
*/
|
||||||
|
export const getAllPrecedingNodes = (
|
||||||
|
currentNodeId: string,
|
||||||
|
allNodes: FlowNode[],
|
||||||
|
allEdges: FlowEdge[]
|
||||||
|
): FlowNode[] => {
|
||||||
|
const precedingNodeIds = new Set<string>();
|
||||||
|
const visited = new Set<string>();
|
||||||
|
|
||||||
|
// DFS 反向遍历
|
||||||
|
const dfs = (nodeId: string) => {
|
||||||
|
if (visited.has(nodeId)) return;
|
||||||
|
visited.add(nodeId);
|
||||||
|
|
||||||
|
// 找到所有指向当前节点的边
|
||||||
|
const incomingEdges = allEdges.filter(e => e.target === nodeId);
|
||||||
|
|
||||||
|
incomingEdges.forEach(edge => {
|
||||||
|
precedingNodeIds.add(edge.source);
|
||||||
|
dfs(edge.source); // 递归向上遍历
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
dfs(currentNodeId);
|
||||||
|
|
||||||
|
// 返回节点对象,排除START节点(它没有输出)
|
||||||
|
return allNodes.filter(node =>
|
||||||
|
precedingNodeIds.has(node.id) &&
|
||||||
|
node.data?.nodeType !== 'START_EVENT'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集指定节点的所有可用变量(来自前序节点的输出)
|
||||||
|
*
|
||||||
|
* @param currentNodeId 当前节点ID
|
||||||
|
* @param allNodes 所有节点列表
|
||||||
|
* @param allEdges 所有连接边列表
|
||||||
|
* @returns 变量列表
|
||||||
|
*/
|
||||||
|
export const collectNodeVariables = (
|
||||||
|
currentNodeId: string,
|
||||||
|
allNodes: FlowNode[],
|
||||||
|
allEdges: FlowEdge[]
|
||||||
|
): NodeVariable[] => {
|
||||||
|
const variables: NodeVariable[] = [];
|
||||||
|
|
||||||
|
// 获取所有前序节点
|
||||||
|
const precedingNodes = getAllPrecedingNodes(currentNodeId, allNodes, allEdges);
|
||||||
|
|
||||||
|
// 遍历前序节点,提取输出字段
|
||||||
|
precedingNodes.forEach(node => {
|
||||||
|
const nodeDefinition = node.data?.nodeDefinition;
|
||||||
|
|
||||||
|
// 检查节点是否有输出定义
|
||||||
|
if (!nodeDefinition || !isConfigurableNode(nodeDefinition) || !nodeDefinition.outputs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取节点显示名称(优先使用用户自定义的label)
|
||||||
|
const nodeName = node.data.label || nodeDefinition.nodeName || 'Unknown';
|
||||||
|
|
||||||
|
// 为每个输出字段创建变量
|
||||||
|
nodeDefinition.outputs.forEach(output => {
|
||||||
|
variables.push({
|
||||||
|
nodeId: node.id,
|
||||||
|
nodeName: nodeName,
|
||||||
|
fieldName: output.name,
|
||||||
|
fieldType: output.type,
|
||||||
|
displayText: `\${${nodeName}.${output.name}}`,
|
||||||
|
fullText: `${nodeName}.${output.name}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按节点分组变量
|
||||||
|
*
|
||||||
|
* @param variables 变量列表
|
||||||
|
* @returns 分组后的变量 Map<节点名称, 变量列表>
|
||||||
|
*/
|
||||||
|
export const groupVariablesByNode = (variables: NodeVariable[]): Map<string, NodeVariable[]> => {
|
||||||
|
const groups = new Map<string, NodeVariable[]>();
|
||||||
|
|
||||||
|
variables.forEach(variable => {
|
||||||
|
const key = variable.nodeName;
|
||||||
|
if (!groups.has(key)) {
|
||||||
|
groups.set(key, []);
|
||||||
|
}
|
||||||
|
groups.get(key)!.push(variable);
|
||||||
|
});
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
};
|
||||||
|
|
||||||
156
frontend/src/utils/workflow/variableConversion.ts
Normal file
156
frontend/src/utils/workflow/variableConversion.ts
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import type { FlowNode } from '@/pages/Workflow/Design/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变量表达式的正则模式
|
||||||
|
* 匹配 ${xxx.yyy} 格式
|
||||||
|
* xxx: 节点名称或UUID
|
||||||
|
* yyy: 字段名
|
||||||
|
*/
|
||||||
|
const VARIABLE_PATTERN = /\$\{([^.}]+)\.([^}]+)\}/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将显示名称格式转换为UUID格式
|
||||||
|
* ${节点名称.字段名} → ${节点UUID.字段名}
|
||||||
|
*
|
||||||
|
* @param displayText 包含显示名称的文本
|
||||||
|
* @param allNodes 所有节点列表(用于查找UUID)
|
||||||
|
* @returns 转换后的UUID格式文本
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* convertToUUID("构建${Jenkins构建.buildStatus}完成", allNodes)
|
||||||
|
* // 返回: "构建${uuid-123.buildStatus}完成"
|
||||||
|
*/
|
||||||
|
export const convertToUUID = (
|
||||||
|
displayText: string,
|
||||||
|
allNodes: FlowNode[]
|
||||||
|
): string => {
|
||||||
|
if (!displayText || typeof displayText !== 'string') {
|
||||||
|
return displayText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayText.replace(VARIABLE_PATTERN, (match, nodeNameOrId, fieldName) => {
|
||||||
|
// 尝试通过名称查找节点
|
||||||
|
const nodeByName = allNodes.find(n => {
|
||||||
|
const nodeName = n.data?.label || n.data?.nodeDefinition?.nodeName;
|
||||||
|
return nodeName === nodeNameOrId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nodeByName) {
|
||||||
|
// 找到了,替换为UUID
|
||||||
|
return `\${${nodeByName.id}.${fieldName}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可能已经是UUID格式,或者节点已被删除
|
||||||
|
// 保持原样
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将UUID格式转换为显示名称格式
|
||||||
|
* ${节点UUID.字段名} → ${节点名称.字段名}
|
||||||
|
*
|
||||||
|
* @param uuidText 包含UUID的文本
|
||||||
|
* @param allNodes 所有节点列表(用于查找显示名称)
|
||||||
|
* @returns 转换后的显示名称格式文本
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* convertToDisplayName("构建${uuid-123.buildStatus}完成", allNodes)
|
||||||
|
* // 返回: "构建${Jenkins构建.buildStatus}完成"
|
||||||
|
*/
|
||||||
|
export const convertToDisplayName = (
|
||||||
|
uuidText: string,
|
||||||
|
allNodes: FlowNode[]
|
||||||
|
): string => {
|
||||||
|
if (!uuidText || typeof uuidText !== 'string') {
|
||||||
|
return uuidText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uuidText.replace(VARIABLE_PATTERN, (match, nodeIdOrName, fieldName) => {
|
||||||
|
// 尝试通过UUID查找节点
|
||||||
|
const nodeById = allNodes.find(n => n.id === nodeIdOrName);
|
||||||
|
|
||||||
|
if (nodeById) {
|
||||||
|
// 找到了,替换为显示名称
|
||||||
|
const nodeName = nodeById.data?.label || nodeById.data?.nodeDefinition?.nodeName || nodeIdOrName;
|
||||||
|
return `\${${nodeName}.${fieldName}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可能已经是显示名称格式,或者节点已被删除
|
||||||
|
// 保持原样
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换对象中的所有字符串值(从显示名称到UUID)
|
||||||
|
* 递归处理嵌套对象和数组
|
||||||
|
*
|
||||||
|
* @param obj 要转换的对象
|
||||||
|
* @param allNodes 所有节点列表
|
||||||
|
* @returns 转换后的对象
|
||||||
|
*/
|
||||||
|
export const convertObjectToUUID = <T extends Record<string, any>>(
|
||||||
|
obj: T,
|
||||||
|
allNodes: FlowNode[]
|
||||||
|
): T => {
|
||||||
|
if (!obj || typeof obj !== 'object') {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: any = Array.isArray(obj) ? [] : {};
|
||||||
|
|
||||||
|
for (const key in obj) {
|
||||||
|
const value = obj[key];
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
// 字符串类型:进行转换
|
||||||
|
result[key] = convertToUUID(value, allNodes);
|
||||||
|
} else if (value && typeof value === 'object') {
|
||||||
|
// 对象或数组:递归转换
|
||||||
|
result[key] = convertObjectToUUID(value, allNodes);
|
||||||
|
} else {
|
||||||
|
// 其他类型:保持原样
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result as T;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换对象中的所有字符串值(从UUID到显示名称)
|
||||||
|
* 递归处理嵌套对象和数组
|
||||||
|
*
|
||||||
|
* @param obj 要转换的对象
|
||||||
|
* @param allNodes 所有节点列表
|
||||||
|
* @returns 转换后的对象
|
||||||
|
*/
|
||||||
|
export const convertObjectToDisplayName = <T extends Record<string, any>>(
|
||||||
|
obj: T,
|
||||||
|
allNodes: FlowNode[]
|
||||||
|
): T => {
|
||||||
|
if (!obj || typeof obj !== 'object') {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: any = Array.isArray(obj) ? [] : {};
|
||||||
|
|
||||||
|
for (const key in obj) {
|
||||||
|
const value = obj[key];
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
// 字符串类型:进行转换
|
||||||
|
result[key] = convertToDisplayName(value, allNodes);
|
||||||
|
} else if (value && typeof value === 'object') {
|
||||||
|
// 对象或数组:递归转换
|
||||||
|
result[key] = convertObjectToDisplayName(value, allNodes);
|
||||||
|
} else {
|
||||||
|
// 其他类型:保持原样
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result as T;
|
||||||
|
};
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user