From 313307a19db762225cce268c3528e9e70dc74b5d Mon Sep 17 00:00:00 2001 From: dengqichen Date: Wed, 22 Oct 2025 13:33:37 +0800 Subject: [PATCH] 1 --- .../src/components/VariableInput/index.tsx | 256 ++++++++++++++++++ .../src/components/VariableInput/types.ts | 48 ++++ .../src/components/VariableInput/utils.ts | 95 +++++++ .../Design/components/EdgeConfigModal.tsx | 52 +++- .../Design/components/NodeConfigModal.tsx | 92 +++++-- frontend/src/pages/Workflow/Design/index.tsx | 4 + .../utils/workflow/collectNodeVariables.ts | 120 ++++++++ .../src/utils/workflow/variableConversion.ts | 156 +++++++++++ 8 files changed, 790 insertions(+), 33 deletions(-) create mode 100644 frontend/src/components/VariableInput/index.tsx create mode 100644 frontend/src/components/VariableInput/types.ts create mode 100644 frontend/src/components/VariableInput/utils.ts create mode 100644 frontend/src/utils/workflow/collectNodeVariables.ts create mode 100644 frontend/src/utils/workflow/variableConversion.ts diff --git a/frontend/src/components/VariableInput/index.tsx b/frontend/src/components/VariableInput/index.tsx new file mode 100644 index 00000000..00885a58 --- /dev/null +++ b/frontend/src/components/VariableInput/index.tsx @@ -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( + + ${`{${match[1]}}`} + + ); + + 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 = ({ + 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(null); + const containerRef = useRef(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) => { + 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) => { + // 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 ; + } + + return