perf: 优化工作流的节点UI和交互

This commit is contained in:
2026-03-01 15:52:22 +08:00
parent 4ef17da6f4
commit 05990072e6
10 changed files with 848 additions and 150 deletions

View File

@@ -3,11 +3,13 @@
import {createFloating} from '../utils/createFloating';
import type {Placement} from '@floating-ui/dom';
const { children, floating, placement = 'bottom' }:
const { children, floating, placement = 'bottom', onShow, onHide }:
{
children: Snippet,
floating: Snippet,
placement?: Placement,
onShow?: () => void,
onHide?: () => void
} = $props();
let triggerEl!: HTMLDivElement, contentEl!: HTMLDivElement;
@@ -18,23 +20,26 @@
trigger: triggerEl,
floatContent: contentEl,
interactive: true,
placement
placement,
onShow,
onHide
});
return () => {
floatingInstance.destroy();
};
});
export function hide() {
floatingInstance.hide();
floatingInstance?.hide();
onHide?.();
}
</script>
<div style="position: relative">
<div bind:this={triggerEl}>
{@render children() }
</div>
<div style="display: none; width: 100%;z-index: 9999" bind:this={contentEl}>
{@render floating() }
</div>
<div style="position: relative" bind:this={triggerEl}>
{@render children() }
</div>
<div style="display: none; width: max-content; z-index: 99999; position: absolute;" bind:this={contentEl}>
{@render floating() }
</div>

View File

@@ -1,30 +1,28 @@
<script lang="ts">
import {FloatingTrigger, Render} from './index';
import {FloatingTrigger} from './index';
import type {SelectItem} from '#types';
import {nodeIcons} from '../../consts';
let {
items,
onSelect,
value = [],
defaultValue = [],
expandAll = true,
multiple = false,
expandValue = [],
placeholder,
variant = 'default',
...rest
}: {
items: SelectItem[],
onSelect?: (item: SelectItem) => void,
value?: (any)[],
defaultValue?: (number | string | undefined)[],
expandAll?: boolean,
expandValue?: (number | string)[],
multiple?: boolean
placeholder?: string
variant?: 'default' | 'reference' | 'model'
[key: string]: any
} = $props();
let activeItemsState = $derived.by(() => {
const resultItems: SelectItem[] = [];
const fillResult = (items: SelectItem[]) => {
@@ -48,50 +46,179 @@
return resultItems;
});
let triggerObject: any;
function handlerOnSelect(item: SelectItem) {
let triggerObject: any = $state();
let hoveredItem: SelectItem | null = $state(null);
let isOpen = $state(false);
function closeMenu() {
triggerObject?.hide();
onSelect?.(item);
isOpen = false;
hoveredItem = null;
}
function handlerOnSelect(item: SelectItem) {
if (item.selectable !== false) {
onSelect?.(item);
closeMenu();
} else {
if (variant === 'reference') {
hoveredItem = item;
} else {
hoveredItem = hoveredItem === item ? null : item;
}
}
}
function handleMouseEnter(item: SelectItem) {
if (variant === 'reference' && item.children && item.children.length > 0) {
hoveredItem = item;
}
}
</script>
{#snippet selectItems(items: SelectItem[])}
{#each items as item, index (`${index}_${item.value}`)}
<button class="tf-select-content-item"
onclick={() => handlerOnSelect(item)}
>
<span>
{#if item.children && item.children.length > 0}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path
d="M12 14L8 10H16L12 14Z"></path></svg>
{/if}
</span>
<Render target={item.label} />
{#snippet renderDefaultItems(items: SelectItem[], depth = 0)}
{#each items as item}
<button class="tf-select-default-item" style="padding-left: {10 + depth * 14}px" onclick={(e) => { e.stopPropagation(); handlerOnSelect(item); }}>
<span class="tf-select-default-item-label">{item.label}</span>
</button>
{#if (item.children && item.children.length > 0 && (expandAll || expandValue.includes(item.value)))}
<div class="tf-select-content-children">
{@render selectItems(item.children)}
{#if item.children && item.children.length > 0}
<div class="tf-select-default-children">
{@render renderDefaultItems(item.children, depth + 1)}
</div>
{/if}
{/each}
{/snippet}
{#snippet renderModelItems(items: SelectItem[])}
{#each items as group}
{#if group.selectable === false}
<div class="tf-select-model-group-title">
{#if group.icon}
<span class="tf-select-model-group-icon">{@html group.icon}</span>
{/if}
<span>{group.label}</span>
</div>
{#each group.children || [] as model}
<button class="tf-select-model-item {value.includes(model.value) ? 'active' : ''}" onclick={(e) => { e.stopPropagation(); handlerOnSelect(model); }}>
<div class="tf-select-model-icon">
{#if model.icon}
{@html model.icon}
{:else}
<div class="tf-select-model-avatar">{group.label ? group.label.toString().charAt(0) : 'M'}</div>
{/if}
</div>
<div class="tf-select-model-info">
<div class="tf-select-model-title">{model.label}</div>
{#if model.tags && model.tags.length > 0}
<div class="tf-select-model-tags">
{#each model.tags as tag}
<span class="tf-select-model-tag">{tag}</span>
{/each}
</div>
{/if}
{#if model.description}
<div class="tf-select-model-desc">{model.description}</div>
{/if}
</div>
</button>
{/each}
{:else}
<button class="tf-select-model-item {value.includes(group.value) ? 'active' : ''}" onclick={(e) => { e.stopPropagation(); handlerOnSelect(group); }}>
<div class="tf-select-model-icon">
{#if group.icon}
{@html group.icon}
{:else}
<div class="tf-select-model-avatar">{group.label ? group.label.toString().charAt(0) : 'M'}</div>
{/if}
</div>
<div class="tf-select-model-info">
<div class="tf-select-model-title">{group.label}</div>
{#if group.tags && group.tags.length > 0}
<div class="tf-select-model-tags">
{#each group.tags as tag}
<span class="tf-select-model-tag">{tag}</span>
{/each}
</div>
{/if}
{#if group.description}
<div class="tf-select-model-desc">{group.description}</div>
{/if}
</div>
</button>
{/if}
{/each}
{/snippet}
{#snippet renderNestedItems(items: SelectItem[], depth = 0)}
{#each items as item}
<div class="tf-select-item-container" style="padding-left: {depth * 16}px">
<button class="tf-select-item {item.children && item.children.length > 0 ? 'has-children' : ''}"
onclick={() => handlerOnSelect(item)}>
<div class="tf-parameter-label">
<div class="tf-parameter-name-wrapper">
{#if item.children && item.children.length > 0}
<span class="tf-parameter-expand-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"></polyline></svg>
</span>
{/if}
<span class="tf-parameter-name">{item.label}</span>
</div>
{#if item.dataType}
<span class="tf-parameter-type">{item.dataType}</span>
{/if}
</div>
</button>
</div>
{#if item.children && item.children.length > 0}
<div class="tf-select-item-children">
{@render renderNestedItems(item.children, depth + 1)}
</div>
{/if}
{/each}
{/snippet}
<div {...rest} class="tf-select {rest['class']}">
<FloatingTrigger bind:this={triggerObject}>
<button class="tf-select-input nopan nodrag" {...rest}>
<FloatingTrigger bind:this={triggerObject} onShow={() => isOpen = true} onHide={() => { isOpen = false; hoveredItem = null; }}>
<button class="tf-select-input nopan nodrag {isOpen ? 'active' : ''}" {...rest}>
<div class="tf-select-input-value">
{#each activeItemsState as item, index (`${index}_${item.value}`)}
{#if !multiple}
{#if index === 0}
<Render target={item.label} />
<div class="tf-parameter-label-input">
{#if variant === 'reference' && item.nodeType && nodeIcons[item.nodeType]}
<span class="tf-select-item-icon-input">
{@html nodeIcons[item.nodeType]}
</span>
{:else if variant === 'model' && item.icon}
<span class="tf-select-item-icon-input-model">
{@html item.icon}
</span>
{/if}
<span class="tf-parameter-name">{item.displayLabel || item.label}</span>
{#if variant === 'reference' && item.dataType}
<span class="tf-parameter-type">{item.dataType}</span>
{/if}
</div>
{/if}
{:else}
<Render target={item.label} />
<div class="tf-parameter-label-input">
{#if variant === 'reference' && item.nodeType && nodeIcons[item.nodeType]}
<span class="tf-select-item-icon-input">
{@html nodeIcons[item.nodeType]}
</span>
{:else if variant === 'model' && item.icon}
<span class="tf-select-item-icon-input-model">
{@html item.icon}
</span>
{/if}
<span class="tf-parameter-name">{item.displayLabel || item.label}</span>
{#if variant === 'reference' && item.dataType}
<span class="tf-parameter-type">{item.dataType}</span>
{/if}
</div>
{#if index < activeItemsState.length - 1}
,
<span style="margin-right: 4px;">,</span>
{/if}
{/if}
{:else}
@@ -101,17 +228,486 @@
{/each}
</div>
<div class="tf-select-input-arrow">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M11.9999 13.1714L16.9497 8.22168L18.3639 9.63589L11.9999 15.9999L5.63599 9.63589L7.0502 8.22168L11.9999 13.1714Z"></path>
</svg>
{#if variant === 'reference'}
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L20.6603 7V17L12 22L3.33975 17V7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<circle cx="12" cy="12" r="2" fill="currentColor"/>
</svg>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M11.9999 13.1714L16.9497 8.22168L18.3639 9.63589L11.9999 15.9999L5.63599 9.63589L7.0502 8.22168L11.9999 13.1714Z"></path>
</svg>
{/if}
</div>
</button>
{#snippet floating()}
<div class="tf-select-content nopan nodrag nowheel ">
{@render selectItems(items)}
</div>
{#if variant === 'default'}
<div class="tf-select-default-wrapper nopan nodrag nowheel">
{@render renderDefaultItems(items)}
</div>
{:else if variant === 'model'}
<div class="tf-select-model-wrapper nopan nodrag nowheel">
<div class="tf-select-model-header">
<span class="tf-select-model-header-title">Model selection</span>
<svg class="tf-select-model-header-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</div>
<div class="tf-select-model-list">
{@render renderModelItems(items)}
</div>
</div>
{:else}
<div class="tf-select-wrapper nopan nodrag nowheel">
<!-- 一级列表:节点/分类 -->
<div class="tf-select-list tf-select-primary-list">
{#each items as item}
<button class="tf-select-item {hoveredItem?.value === item.value ? 'active' : ''}"
onmouseenter={() => handleMouseEnter(item)}
onclick={() => handlerOnSelect(item)}
>
<span class="tf-select-item-icon">
{#if item.icon}
{@html item.icon}
{:else if item.nodeType && nodeIcons[item.nodeType]}
{@html nodeIcons[item.nodeType]}
{:else if item.children && item.children.length > 0}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 7h18M3 12h18M3 17h18"></path></svg>
{/if}
</span>
<span class="tf-select-item-label">{item.label}</span>
{#if item.children && item.children.length > 0}
<span class="tf-select-item-arrow">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
</span>
{/if}
</button>
{/each}
</div>
<!-- 二级列表:树形展示参数及其嵌套子项 -->
{#if hoveredItem && hoveredItem.children && hoveredItem.children.length > 0}
<div class="tf-select-list tf-select-secondary-list">
{@render renderNestedItems(hoveredItem.children)}
</div>
{/if}
</div>
{/if}
{/snippet}
</FloatingTrigger>
</div>
<style lang="less">
.tf-select-default-wrapper {
display: flex;
flex-direction: column;
background: #fff;
margin-top: 5px;
border: 1px solid #ccc;
border-radius: 5px;
padding: 5px;
width: max-content;
min-width: 100%;
z-index: 99999;
box-sizing: border-box;
max-height: 220px;
overflow: auto;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.tf-select-default-item {
display: flex;
align-items: center;
padding: 5px 10px;
border: none;
background: #fff;
border-radius: 5px;
cursor: pointer;
line-height: 100%;
gap: 2px;
width: 100%;
text-align: left;
font-size: 13px;
color: #333;
&:hover {
background: #f0f0f0;
}
}
.tf-select-default-children {
display: flex;
flex-direction: column;
}
/* Model Variant Styles */
.tf-select-model-wrapper {
display: flex;
flex-direction: column;
background: #fff;
margin-top: 5px;
border: 1px solid #e5e7eb;
border-radius: 10px; /* slightly smaller radius */
width: 280px; /* Reduced width to be more compact */
z-index: 99999;
box-sizing: border-box;
max-height: 400px; /* slightly less tall */
overflow: hidden;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
}
.tf-select-model-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px; /* tighter padding */
border-bottom: 1px solid #f3f4f6;
background: #fff;
}
.tf-select-model-header-title {
font-size: 13px; /* smaller title */
font-weight: 600;
color: #111827;
}
.tf-select-model-header-icon {
width: 14px;
height: 14px;
color: #6b7280;
cursor: pointer;
}
.tf-select-model-list {
display: flex;
flex-direction: column;
padding: 8px; /* tighter padding */
overflow-y: auto;
gap: 4px; /* tighter gap between items */
}
.tf-select-model-group-title {
font-size: 12px; /* smaller group title */
font-weight: 500;
color: #6b7280;
margin-top: 6px;
margin-bottom: 2px;
padding-left: 6px;
display: flex;
align-items: center;
gap: 6px;
}
.tf-select-model-group-icon {
width: 14px; /* smaller icon */
height: 14px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.tf-select-model-group-icon :global(svg), .tf-select-model-group-icon img {
width: 100%;
height: 100%;
object-fit: contain;
}
.tf-select-model-group-title:first-child {
margin-top: 0;
}
.tf-select-model-item {
display: flex;
align-items: flex-start;
padding: 6px 8px; /* tighter item padding */
border: none;
background: transparent;
border-radius: 6px;
cursor: pointer;
text-align: left;
gap: 10px;
transition: all 0.2s;
}
.tf-select-model-item:hover {
background: #f9fafb;
}
.tf-select-model-item.active {
background: #e0e7ff;
}
.tf-select-model-icon {
flex-shrink: 0;
width: 32px; /* smaller item icon */
height: 32px;
border-radius: 6px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: #f3f4f6;
margin-top: 2px;
}
.tf-select-model-icon :global(svg), .tf-select-model-icon img {
width: 70%; /* icon inside box */
height: 70%;
object-fit: contain;
}
.tf-select-model-avatar {
font-size: 14px;
font-weight: 600;
color: #9ca3af;
}
.tf-select-model-info {
display: flex;
flex-direction: column;
gap: 2px; /* tighter info gap */
flex-grow: 1;
overflow: hidden;
}
.tf-select-model-title {
font-size: 13px; /* smaller font */
font-weight: 500;
color: #111827;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tf-select-model-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.tf-select-model-tag {
background: #f3f4f6;
color: #6b7280;
font-size: 10px; /* smaller tag */
padding: 1px 6px;
border-radius: 4px;
}
.tf-select-model-desc {
font-size: 11px; /* smaller desc */
color: #6b7280;
line-height: 1.3;
margin-top: 2px;
display: -webkit-box;
-webkit-line-clamp: 2; /* limit lines */
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Reference Variant Styles */
.tf-select-wrapper {
display: flex;
background: #fff;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
border: 1px solid #e5e7eb;
overflow: hidden;
min-width: 220px;
width: max-content;
max-width: 600px;
max-height: 480px;
transition: width 0.2s ease;
z-index: 99999;
}
.tf-select-list {
display: flex;
flex-direction: column;
padding: 8px;
overflow-y: auto;
&.tf-select-primary-list {
width: 220px;
flex-shrink: 0;
background: #f9fafb;
}
&.tf-select-secondary-list {
flex-grow: 1;
min-width: 200px;
background: #fff;
padding: 12px;
border-left: 1px solid #f3f4f6;
animation: slideIn 0.2s ease-out;
}
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
.tf-select-item-container {
position: relative;
}
.tf-select-item-children {
position: relative;
&::before {
content: '';
position: absolute;
left: 8px;
top: 0;
bottom: 0;
width: 1px;
background: #f0f0f0;
}
}
.tf-select-item {
display: flex;
align-items: center;
padding: 8px 12px;
border: none;
background: transparent;
border-radius: 8px;
cursor: pointer;
text-align: left;
font-size: 13px;
color: #374151;
gap: 10px;
width: 100%;
transition: all 0.15s;
margin-bottom: 2px;
&:hover {
background: #f3f4f6;
}
&.active {
background: #eff6ff;
color: #2563eb;
font-weight: 500;
}
&.has-children {
background: #f8fafc;
margin-bottom: 4px;
&:hover {
background: #f1f5f9;
}
}
&-icon {
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: #3474ff;
background: #cedafb;
border-radius: 5px;
padding: 3px;
box-sizing: border-box;
:global(svg) {
width: 16px;
height: 16px;
}
}
&-label {
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&-arrow {
width: 14px;
height: 14px;
color: #9ca3af;
}
}
.tf-parameter-label-input {
display: flex;
align-items: center;
gap: 6px;
margin-right: 4px;
.tf-select-item-icon-input {
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: #3474ff;
background: #cedafb;
border-radius: 4px;
padding: 2px;
box-sizing: border-box;
:global(svg) {
width: 12px;
height: 12px;
}
}
.tf-select-item-icon-input-model {
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border-radius: 4px;
overflow: hidden;
:global(svg), img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
}
.tf-parameter-label {
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
gap: 8px;
}
.tf-parameter-name-wrapper {
display: flex;
align-items: center;
gap: 6px;
}
.tf-parameter-expand-icon {
width: 12px;
height: 12px;
color: currentColor;
opacity: 0.6;
}
.tf-parameter-name {
color: inherit;
}
.tf-parameter-type {
background: rgba(0, 0, 0, 0.06);
color: #6b7280;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
line-height: 1.2;
white-space: nowrap;
}
</style>

View File

@@ -94,7 +94,7 @@
{#if param.refType === 'fixed'}
<Input value={param.value} placeholder="请输入参数值" oninput={(event)=>updateParamByEvent('value', event)} />
{:else if (param.refType !== 'input')}
<Select items={selectItems.current} style="width: 100%" defaultValue={["ref"]} value={[param.ref]}
<Select items={selectItems.current} style="width: 100%" defaultValue={["ref"]} value={[param.ref]} variant="reference"
expandAll
onSelect={updateRef} />
{/if}

View File

@@ -95,7 +95,7 @@
{#if param.refType === 'fixed'}
<Input value={param.value} placeholder="请输入参数值" oninput={(event)=>updateParamByEvent('value', event)} />
{:else if (param.refType !== 'input')}
<Select items={selectItems.current} style="width: 100%" defaultValue={["ref"]} value={[param.ref]}
<Select items={selectItems.current} style="width: 100%" defaultValue={["ref"]} value={[param.ref]} variant="reference"
expandAll
onSelect={updateRef} />
{/if}

View File

@@ -29,7 +29,53 @@
let llmArray = $state<SelectItem[]>([]);
onMount(async () => {
const newLLMs = await options.provider?.llm?.();
llmArray.push(...(newLLMs || []));
const isFlat = newLLMs?.every(item => !item.children);
if (isFlat && newLLMs && newLLMs.length > 0) {
const grouped = new Map<string, SelectItem[]>();
for (const llm of newLLMs) {
// If it still has a slash, parse it; otherwise, check if there's a custom logic we can infer brand.
// In WorkflowDesign we pass `item.modelProvider?.providerName` via some other way, but here it's flat.
// Actually, the label is just the title now (e.g. 'deepseek-chat').
// Wait, LLMNode doesn't know the brand unless it's in the string or we modify WorkflowDesign to pass `brand`.
// Let's modify WorkflowDesign to pass `brand` instead.
let brand = (llm as any).brand || '其他';
let modelName = llm.label;
if (!grouped.has(brand)) {
grouped.set(brand, []);
}
grouped.get(brand)!.push({
...llm,
label: modelName,
displayLabel: modelName // 外部选中时也只显示模型名称
});
}
const treeArray: SelectItem[] = [];
for (const [brand, models] of grouped) {
// Try to get a representative icon for the brand from its children
let groupIcon = undefined;
if (models.length > 0) {
const modelWithIcon = models.find(m => m.icon);
if (modelWithIcon) {
groupIcon = modelWithIcon.icon;
}
}
treeArray.push({
label: brand,
value: 'group_' + brand,
selectable: false,
icon: groupIcon,
children: models
});
}
llmArray.push(...treeArray);
} else {
llmArray.push(...(newLLMs || []));
}
});
const { updateNodeData } = useSvelteFlow();
@@ -116,7 +162,7 @@
<Heading level={3} mt="10px">模型设置</Heading>
<div class="setting-title">模型</div>
<div class="setting-item">
<Select items={llmArray} style="width: 100%" placeholder="请选择模型" onSelect={(item)=>{
<Select items={llmArray} variant="model" style="width: 100%" placeholder="请选择模型" onSelect={(item)=>{
const newValue = item.value;
updateNodeData(currentNodeId, ()=>{
return {

View File

@@ -1,7 +1,6 @@
import {
arrow,
computePosition,
flip,
type FlipOptions,
offset,
type OffsetOptions,
@@ -20,6 +19,8 @@ export type FloatingOptions = {
shiftOptions?: ShiftOptions;
interactive?: boolean;
showArrow?: boolean;
onShow?: () => void;
onHide?: () => void;
};
export type FloatingInstance = {
@@ -37,7 +38,9 @@ export const createFloating = ({
flipOptions,
shiftOptions,
interactive,
showArrow
showArrow,
onShow,
onHide
}: FloatingOptions): FloatingInstance => {
if (typeof trigger === 'string') {
const triggerEl = document.querySelector(trigger);
@@ -78,14 +81,15 @@ export const createFloating = ({
placement: placement,
middleware: [
offset(offsetOptions), // 手动偏移配置
flip(flipOptions), //自动翻转
// flip(flipOptions), // 注释掉自动翻转,强制向下弹出,避免遮挡顶部工具栏
shift(shiftOptions), //自动偏移(使得浮动元素能够进入视野)
...(showArrow ? [arrow({ element: arrowElement })] : [])
]
}).then(({ x, y, placement, middlewareData }) => {
Object.assign(floating.style, {
left: `${x}px`,
top: `${y}px`
top: `${y}px`,
position: 'absolute'
});
if (showArrow) {
@@ -113,7 +117,7 @@ export const createFloating = ({
function showTooltip() {
floating.style.display = 'block';
floating.style.visibility = 'block';
floating.style.visibility = 'visible';
floating.style.position = 'absolute';
if (showArrow) {
@@ -122,6 +126,7 @@ export const createFloating = ({
visible = true;
updatePosition();
onShow?.();
}
function hideTooltip() {
@@ -130,10 +135,10 @@ export const createFloating = ({
arrowElement.style.display = 'none';
}
visible = false;
onHide?.();
}
function onTrigger(event: any) {
event.stopPropagation();
if (!visible) {
showTooltip();
} else {
@@ -142,7 +147,7 @@ export const createFloating = ({
}
function hideTooltipCompute(event: any) {
if (floating.contains(event.target as Node)) {
if (floating.contains(event.target as Node) || (trigger as Node).contains(event.target as Node)) {
return;
}
hideTooltip();

View File

@@ -1,6 +1,7 @@
import {type Edge, type Node, useNodesData, useStore} from '@xyflow/svelte';
import type {Parameter} from '#types';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {getCurrentNodeId, getOptions} from '#components/utils/NodeUtils';
import {nodeIcons} from '../../consts';
const fillRefNodeIds = (refNodeIds: string[], currentNodeId: string, edges: Edge[]) => {
for (const edge of edges) {
@@ -11,51 +12,82 @@ const fillRefNodeIds = (refNodeIds: string[], currentNodeId: string, edges: Edge
}
};
const getChildren = (params: any, parentId: string, nodeIsChildren: boolean) => {
const getChildren = (params: any, parentId: string, nodeIsChildren: boolean, nodeType: string) => {
if (!params || params.length === 0) return [];
return params.map((param: any) => ({
label:
param.name +
(nodeIsChildren
? ` (Array<${param.dataType || 'String'}>)`
: ` (${param.dataType || 'String'})`),
value: parentId + '.' + param.name,
children: getChildren(param.children, parentId + '.' + param.name, nodeIsChildren)
}));
return params.map((param: any) => {
const dataType = nodeIsChildren
? `Array<${param.dataType || 'String'}>`
: (param.dataType || 'String');
return {
label: param.name,
dataType: dataType,
value: parentId + '.' + param.name,
selectable: true,
nodeType: nodeType,
children: getChildren(param.children, parentId + '.' + param.name, nodeIsChildren, nodeType)
};
});
};
const nodeToOptions = (node: Node, nodeIsChildren: boolean, currentNode: Node) => {
const options = getOptions();
let icon = nodeIcons[node.type];
if (!icon && options?.customNodes && options.customNodes[node.type]) {
icon = options.customNodes[node.type].icon;
}
// 如果仍然获取不到,尝试使用 data.icon (作为回退)
if (!icon && node.data && node.data.icon) {
icon = node.data.icon as string;
}
const title = node.data.title;
if (node.type === 'startNode') {
const parameters = node.data.parameters as Array<Parameter>;
const children = [];
if (parameters)
for (const parameter of parameters) {
const dataType = nodeIsChildren
? `Array<${parameter.dataType || 'String'}>`
: (parameter.dataType || 'String');
children.push({
label:
parameter.name +
(nodeIsChildren
? ` (Array<${parameter.dataType || 'String'}>)`
: ` (${parameter.dataType || 'String'})`),
value: node.id + '.' + parameter.name
label: parameter.name,
dataType: dataType,
value: node.id + '.' + parameter.name,
selectable: true,
nodeType: node.type
});
}
return {
label: node.data.title,
label: title,
icon: icon,
value: node.id,
selectable: false,
nodeType: node.type,
children
};
} else if (node.type === 'loopNode' && currentNode.parentId) {
return {
label: node.data.title,
label: title,
icon: icon,
value: node.id,
selectable: false,
nodeType: node.type,
children: [
{
label: 'loopItem',
value: node.id + '.loopItem'
dataType: 'Any',
value: node.id + '.loopItem',
selectable: true,
nodeType: node.type
},
{
label: 'index (Number)',
value: node.id + '.index'
label: 'index',
dataType: 'Number',
value: node.id + '.index',
selectable: true,
nodeType: node.type
}
]
};
@@ -63,9 +95,12 @@ const nodeToOptions = (node: Node, nodeIsChildren: boolean, currentNode: Node) =
const outputDefs = node.data.outputDefs;
if (outputDefs) {
return {
label: node.data.title,
label: title,
icon: icon,
value: node.id,
children: getChildren(outputDefs, node.id, nodeIsChildren)
selectable: false,
nodeType: node.type,
children: getChildren(outputDefs, node.id, nodeIsChildren, node.type)
};
}
}