feat: 优化工作流参数编辑与告警交互
- 新增 ParamTokenEditor,支持参数选择插入、token 高亮、整段删除与光标避让 - 参数候选改为动态监测,未映射参数可选择并在下拉与输入框顶部告警 - 接入知识库/搜索引擎/LLM/动态代码/HTTP Body 及 SQL、查询数据自定义节点 - 优化 Http 节点布局并补充参数解析工具与单测
This commit is contained in:
@@ -47,6 +47,7 @@ export const SearchDatacenterNode = async () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
|
templateSupport: true,
|
||||||
label: "如:name='张三' and age=21 or field = {{流程变量}}",
|
label: "如:name='张三' and age=21 or field = {{流程变量}}",
|
||||||
description: '',
|
description: '',
|
||||||
name: 'where',
|
name: 'where',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default {
|
|||||||
{
|
{
|
||||||
name: 'sql',
|
name: 'sql',
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
|
templateSupport: true,
|
||||||
label: 'SQL',
|
label: 'SQL',
|
||||||
placeholder: $t('aiWorkflow.descriptions.enterSQL'),
|
placeholder: $t('aiWorkflow.descriptions.enterSQL'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,748 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { tick } from 'svelte';
|
||||||
|
import {Button, FloatingTrigger} from '../base';
|
||||||
|
import type {Parameter} from '#types';
|
||||||
|
import {
|
||||||
|
escapeHtml,
|
||||||
|
flattenParameterCandidates,
|
||||||
|
findBackspaceTokenRange,
|
||||||
|
findTokenRangeAtCursor,
|
||||||
|
insertTextAtCursor,
|
||||||
|
parseTokenParts,
|
||||||
|
splitTokenDisplay
|
||||||
|
} from '../utils/paramToken';
|
||||||
|
|
||||||
|
const {
|
||||||
|
mode = 'textarea',
|
||||||
|
value = '',
|
||||||
|
parameters = [],
|
||||||
|
rows = 3,
|
||||||
|
placeholder = '',
|
||||||
|
disabled = false,
|
||||||
|
class: className = '',
|
||||||
|
style = '',
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
mode?: 'input' | 'textarea';
|
||||||
|
value?: string;
|
||||||
|
parameters?: Parameter[];
|
||||||
|
rows?: number;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
class?: string;
|
||||||
|
style?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let inputEl = $state<HTMLInputElement | null>(null);
|
||||||
|
let textareaEl = $state<HTMLTextAreaElement | null>(null);
|
||||||
|
let highlightEl = $state<HTMLDivElement | null>(null);
|
||||||
|
let triggerObject: any;
|
||||||
|
let isFocused = $state(false);
|
||||||
|
let isComposing = $state(false);
|
||||||
|
let localValue = $state((value || '') as string);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
const nextValue = (value || '') as string;
|
||||||
|
if (!isFocused && nextValue !== localValue) {
|
||||||
|
localValue = nextValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const paramCandidates = $derived(flattenParameterCandidates(parameters));
|
||||||
|
const paramNames = $derived(paramCandidates.map((item) => item.name));
|
||||||
|
const unresolvedParamSet = $derived.by(() => {
|
||||||
|
return new Set(paramCandidates.filter((item) => !item.resolved).map((item) => item.name));
|
||||||
|
});
|
||||||
|
const hasParams = $derived(paramCandidates.length > 0);
|
||||||
|
const tokenParts = $derived(parseTokenParts(localValue, paramNames));
|
||||||
|
const unresolvedTokensInEditor = $derived.by(() => {
|
||||||
|
const keySet = new Set<string>();
|
||||||
|
for (const part of tokenParts) {
|
||||||
|
if (part.type === 'token' && part.valid && unresolvedParamSet.has(part.key)) {
|
||||||
|
keySet.add(part.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(keySet);
|
||||||
|
});
|
||||||
|
const undefinedTokensInEditor = $derived.by(() => {
|
||||||
|
const keySet = new Set<string>();
|
||||||
|
for (const part of tokenParts) {
|
||||||
|
if (part.type === 'token' && !part.valid) {
|
||||||
|
keySet.add(part.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(keySet);
|
||||||
|
});
|
||||||
|
const showInlineHint = $derived(unresolvedTokensInEditor.length > 0 || undefinedTokensInEditor.length > 0);
|
||||||
|
const highlightedHtml = $derived.by(() => {
|
||||||
|
return tokenParts
|
||||||
|
.map((part) => {
|
||||||
|
if (part.type === 'text') {
|
||||||
|
return escapeHtml(part.text);
|
||||||
|
}
|
||||||
|
const cssClass = !part.valid
|
||||||
|
? 'param-token-invalid'
|
||||||
|
: unresolvedParamSet.has(part.key)
|
||||||
|
? 'param-token-warning'
|
||||||
|
: 'param-token-valid';
|
||||||
|
const display = splitTokenDisplay(part.text, part.key);
|
||||||
|
return `<span class="param-token-token"><span class="param-token-chip ${cssClass}"><span class="param-token-hidden">${escapeHtml(display.hiddenPrefix)}</span><span class="param-token-chip-text">${escapeHtml(display.visibleText)}</span><span class="param-token-hidden">${escapeHtml(display.hiddenSuffix)}</span></span></span>`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
});
|
||||||
|
|
||||||
|
const getEditorElement = () => {
|
||||||
|
return mode === 'input' ? inputEl : textareaEl;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emitInput = (event?: Event) => {
|
||||||
|
if (event) {
|
||||||
|
rest.oninput?.(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rest.oninput?.({
|
||||||
|
target: {
|
||||||
|
value: localValue
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const emitChange = (event?: Event) => {
|
||||||
|
if (event) {
|
||||||
|
rest.onchange?.(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rest.onchange?.({
|
||||||
|
target: {
|
||||||
|
value: localValue
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncScroll = () => {
|
||||||
|
const el = getEditorElement();
|
||||||
|
if (!el || !highlightEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
highlightEl.scrollTop = el.scrollTop;
|
||||||
|
highlightEl.scrollLeft = el.scrollLeft;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInput = (event: Event) => {
|
||||||
|
localValue = ((event.target as HTMLInputElement | HTMLTextAreaElement).value || '') as string;
|
||||||
|
syncScroll();
|
||||||
|
emitInput(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (event: Event) => {
|
||||||
|
localValue = ((event.target as HTMLInputElement | HTMLTextAreaElement).value || '') as string;
|
||||||
|
emitChange(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertParam = async (paramName: string) => {
|
||||||
|
const editorEl = getEditorElement();
|
||||||
|
const result = insertTextAtCursor(
|
||||||
|
localValue,
|
||||||
|
`{{${paramName}}}`,
|
||||||
|
editorEl?.selectionStart,
|
||||||
|
editorEl?.selectionEnd
|
||||||
|
);
|
||||||
|
localValue = result.value;
|
||||||
|
emitInput();
|
||||||
|
emitChange();
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
const newEditorEl = getEditorElement();
|
||||||
|
if (newEditorEl) {
|
||||||
|
newEditorEl.focus();
|
||||||
|
newEditorEl.setSelectionRange(result.cursor, result.cursor);
|
||||||
|
}
|
||||||
|
syncScroll();
|
||||||
|
triggerObject?.hide?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = (event: Event) => {
|
||||||
|
isFocused = true;
|
||||||
|
rest.onfocus?.(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = (event: Event) => {
|
||||||
|
isFocused = false;
|
||||||
|
rest.onblur?.(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCompositionStart = (event: Event) => {
|
||||||
|
isComposing = true;
|
||||||
|
rest.oncompositionstart?.(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCompositionEnd = (event: Event) => {
|
||||||
|
isComposing = false;
|
||||||
|
rest.oncompositionend?.(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCaretPosition = (position: number) => {
|
||||||
|
const editorEl = getEditorElement();
|
||||||
|
if (!editorEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editorEl.setSelectionRange(position, position);
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeCaretAsync = () => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
moveCaretOutOfToken();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveCaretOutOfToken = (prefer?: 'start' | 'end') => {
|
||||||
|
const editorEl = getEditorElement();
|
||||||
|
if (!editorEl) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectionStart = editorEl.selectionStart ?? 0;
|
||||||
|
const selectionEnd = editorEl.selectionEnd ?? 0;
|
||||||
|
if (selectionStart !== selectionEnd) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenRange = findTokenRangeAtCursor(localValue, selectionStart, {
|
||||||
|
includeStart: false,
|
||||||
|
includeEnd: false
|
||||||
|
});
|
||||||
|
if (!tokenRange) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target =
|
||||||
|
prefer === 'start'
|
||||||
|
? tokenRange.start
|
||||||
|
: prefer === 'end'
|
||||||
|
? tokenRange.end
|
||||||
|
: selectionStart - tokenRange.start <= tokenRange.end - selectionStart
|
||||||
|
? tokenRange.start
|
||||||
|
: tokenRange.end;
|
||||||
|
editorEl.setSelectionRange(target, target);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeydown = async (event: KeyboardEvent) => {
|
||||||
|
if (disabled || isComposing) {
|
||||||
|
rest.onkeydown?.(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = event.target as HTMLInputElement | HTMLTextAreaElement;
|
||||||
|
const selectionStart = target.selectionStart ?? 0;
|
||||||
|
const selectionEnd = target.selectionEnd ?? 0;
|
||||||
|
|
||||||
|
if (selectionStart !== selectionEnd) {
|
||||||
|
rest.onkeydown?.(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowLeft') {
|
||||||
|
const leftToken = findTokenRangeAtCursor(localValue, selectionStart, {
|
||||||
|
includeStart: false,
|
||||||
|
includeEnd: true
|
||||||
|
});
|
||||||
|
if (leftToken && leftToken.end === selectionStart) {
|
||||||
|
event.preventDefault();
|
||||||
|
setCaretPosition(leftToken.start);
|
||||||
|
rest.onkeydown?.(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (moveCaretOutOfToken('start')) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
rest.onkeydown?.(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowRight') {
|
||||||
|
const rightToken = findTokenRangeAtCursor(localValue, selectionStart, {
|
||||||
|
includeStart: true,
|
||||||
|
includeEnd: false
|
||||||
|
});
|
||||||
|
if (rightToken && rightToken.start === selectionStart) {
|
||||||
|
event.preventDefault();
|
||||||
|
setCaretPosition(rightToken.end);
|
||||||
|
rest.onkeydown?.(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (moveCaretOutOfToken('end')) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
rest.onkeydown?.(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenRangeInside = findTokenRangeAtCursor(localValue, selectionStart, {
|
||||||
|
includeStart: false,
|
||||||
|
includeEnd: false
|
||||||
|
});
|
||||||
|
if (tokenRangeInside && event.key !== 'Backspace' && event.key !== 'Delete') {
|
||||||
|
event.preventDefault();
|
||||||
|
moveCaretOutOfToken();
|
||||||
|
rest.onkeydown?.(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Delete') {
|
||||||
|
const deleteRange = findTokenRangeAtCursor(localValue, selectionStart, {
|
||||||
|
includeStart: true,
|
||||||
|
includeEnd: false
|
||||||
|
});
|
||||||
|
if (deleteRange) {
|
||||||
|
event.preventDefault();
|
||||||
|
const result = insertTextAtCursor(localValue, '', deleteRange.start, deleteRange.end);
|
||||||
|
localValue = result.value;
|
||||||
|
emitInput();
|
||||||
|
emitChange();
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
const editorEl = getEditorElement();
|
||||||
|
if (editorEl) {
|
||||||
|
editorEl.focus();
|
||||||
|
editorEl.setSelectionRange(deleteRange.start, deleteRange.start);
|
||||||
|
}
|
||||||
|
syncScroll();
|
||||||
|
rest.onkeydown?.(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key !== 'Backspace') {
|
||||||
|
rest.onkeydown?.(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenRange = findBackspaceTokenRange(localValue, selectionStart);
|
||||||
|
if (!tokenRange) {
|
||||||
|
rest.onkeydown?.(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
const result = insertTextAtCursor(localValue, '', tokenRange.start, tokenRange.end);
|
||||||
|
localValue = result.value;
|
||||||
|
emitInput();
|
||||||
|
emitChange();
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
const editorEl = getEditorElement();
|
||||||
|
if (editorEl) {
|
||||||
|
editorEl.focus();
|
||||||
|
editorEl.setSelectionRange(tokenRange.start, tokenRange.start);
|
||||||
|
}
|
||||||
|
syncScroll();
|
||||||
|
rest.onkeydown?.(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (event: Event) => {
|
||||||
|
normalizeCaretAsync();
|
||||||
|
rest.onselect?.(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = (event: MouseEvent) => {
|
||||||
|
normalizeCaretAsync();
|
||||||
|
rest.onmouseup?.(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyup = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight' || event.key === 'Home' || event.key === 'End') {
|
||||||
|
normalizeCaretAsync();
|
||||||
|
}
|
||||||
|
rest.onkeyup?.(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = (event: MouseEvent) => {
|
||||||
|
normalizeCaretAsync();
|
||||||
|
rest.onclick?.(event);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="param-token-editor {className}" style={style}>
|
||||||
|
{#if showInlineHint}
|
||||||
|
<div class="param-token-inline-hint">
|
||||||
|
{#if unresolvedTokensInEditor.length > 0}
|
||||||
|
<div class="hint-item hint-unresolved">
|
||||||
|
<span class="hint-item-icon" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12 2C6.486 2 2 6.486 2 12S6.486 22 12 22 22 17.514 22 12 17.514 2 12 2zm0 15a1.25 1.25 0 1 1 0-2.5A1.25 1.25 0 0 1 12 17zm1-4h-2V7h2v6z"></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
未映射参数:{unresolvedTokensInEditor.join('、')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if undefinedTokensInEditor.length > 0}
|
||||||
|
<div class="hint-item hint-undefined">
|
||||||
|
<span class="hint-item-icon" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12 2C6.486 2 2 6.486 2 12S6.486 22 12 22 22 17.514 22 12 17.514 2 12 2zm0 15a1.25 1.25 0 1 1 0-2.5A1.25 1.25 0 0 1 12 17zm1-4h-2V7h2v6z"></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
未定义参数:{undefinedTokensInEditor.join('、')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="param-token-editor-inner">
|
||||||
|
<div
|
||||||
|
class="param-token-editor-highlight {mode === 'input' ? 'single-line' : 'multi-line'}"
|
||||||
|
bind:this={highlightEl}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{@html highlightedHtml}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if mode === 'input'}
|
||||||
|
<input
|
||||||
|
{...rest}
|
||||||
|
bind:this={inputEl}
|
||||||
|
type="text"
|
||||||
|
class="tf-input param-token-input {rest.class}"
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={localValue}
|
||||||
|
disabled={disabled}
|
||||||
|
spellcheck="false"
|
||||||
|
onfocus={handleFocus}
|
||||||
|
onblur={handleBlur}
|
||||||
|
oninput={handleInput}
|
||||||
|
onchange={handleChange}
|
||||||
|
onscroll={syncScroll}
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
onselect={handleSelect}
|
||||||
|
onmouseup={handleMouseUp}
|
||||||
|
onclick={handleClick}
|
||||||
|
onkeyup={handleKeyup}
|
||||||
|
oncompositionstart={handleCompositionStart}
|
||||||
|
oncompositionend={handleCompositionEnd}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<textarea
|
||||||
|
{...rest}
|
||||||
|
bind:this={textareaEl}
|
||||||
|
class="tf-textarea param-token-textarea {rest.class}"
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={localValue}
|
||||||
|
rows={rows}
|
||||||
|
disabled={disabled}
|
||||||
|
spellcheck="false"
|
||||||
|
onfocus={handleFocus}
|
||||||
|
onblur={handleBlur}
|
||||||
|
oninput={handleInput}
|
||||||
|
onchange={handleChange}
|
||||||
|
onscroll={syncScroll}
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
onselect={handleSelect}
|
||||||
|
onmouseup={handleMouseUp}
|
||||||
|
onclick={handleClick}
|
||||||
|
onkeyup={handleKeyup}
|
||||||
|
oncompositionstart={handleCompositionStart}
|
||||||
|
oncompositionend={handleCompositionEnd}
|
||||||
|
></textarea>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="param-token-action">
|
||||||
|
<FloatingTrigger placement="bottom-end" bind:this={triggerObject}>
|
||||||
|
<Button
|
||||||
|
class="param-token-button"
|
||||||
|
disabled={!hasParams || disabled}
|
||||||
|
title={hasParams ? '选择参数' : '请先在输入参数中定义参数'}
|
||||||
|
>
|
||||||
|
<span>{"{x}"}</span>
|
||||||
|
</Button>
|
||||||
|
{#snippet floating()}
|
||||||
|
<div class="param-token-panel nowheel">
|
||||||
|
{#if hasParams}
|
||||||
|
{#each paramCandidates as candidate}
|
||||||
|
<button
|
||||||
|
class="param-token-item {candidate.resolved ? '' : 'unresolved'}"
|
||||||
|
title={candidate.resolved ? candidate.name : `${candidate.name}(未配置引用值)`}
|
||||||
|
onclick={() => {
|
||||||
|
insertParam(candidate.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{candidate.name}</span>
|
||||||
|
{#if !candidate.resolved}
|
||||||
|
<span
|
||||||
|
class="param-token-item-warn"
|
||||||
|
title="该参数尚未映射引用值,运行时可能为空"
|
||||||
|
aria-label="未映射参数"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12 2 1 21h22L12 2zm0 5.2 6.4 11.8H5.6L12 7.2zm-1 3.3v4.6h2v-4.6h-2zm0 5.8v1.9h2v-1.9h-2z"></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<div class="param-token-empty">请先在输入参数中定义参数</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</FloatingTrigger>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.param-token-editor {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-token-inline-hint {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-item-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-item.hint-unresolved {
|
||||||
|
background: #fff8e6;
|
||||||
|
border-color: #f6d99a;
|
||||||
|
color: #8a5a00;
|
||||||
|
|
||||||
|
.hint-item-icon {
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-item.hint-undefined {
|
||||||
|
background: #fff1f1;
|
||||||
|
border-color: #f5c7c4;
|
||||||
|
color: #b42318;
|
||||||
|
|
||||||
|
.hint-item-icon {
|
||||||
|
color: #d92d20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-token-editor-inner {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-token-editor-highlight {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #333;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: 1.6;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-token-editor-highlight.single-line {
|
||||||
|
white-space: pre;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-token-editor-highlight.multi-line {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-token-input,
|
||||||
|
.param-token-textarea {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
background: transparent;
|
||||||
|
color: transparent;
|
||||||
|
caret-color: #333;
|
||||||
|
padding-right: 36px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: 1.6;
|
||||||
|
letter-spacing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-token-input::placeholder,
|
||||||
|
.param-token-textarea::placeholder {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-token-textarea {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-token-action {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.param-token-button) {
|
||||||
|
height: 20px;
|
||||||
|
min-width: 20px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #666;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.param-token-button:hover) {
|
||||||
|
background: #f0f3ff;
|
||||||
|
color: #2563eb;
|
||||||
|
border-color: #d3defd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-token-panel {
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 320px;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow: auto;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-token-item {
|
||||||
|
border: none;
|
||||||
|
background: #fff;
|
||||||
|
padding: 6px 8px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: #333;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-token-item:hover {
|
||||||
|
background: #f5f7ff;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-token-item.unresolved {
|
||||||
|
background: #fff8e6;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-token-item-warn {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #d97706;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-token-empty {
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.param-token-hidden) {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.param-token-token) {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.param-token-chip) {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: baseline;
|
||||||
|
line-height: inherit;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
box-decoration-break: clone;
|
||||||
|
-webkit-box-decoration-break: clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.param-token-chip-text) {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.param-token-valid) {
|
||||||
|
background: #eaf2ff;
|
||||||
|
color: #1e4ed8;
|
||||||
|
box-shadow: inset 0 0 0 1px #c6d8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.param-token-invalid) {
|
||||||
|
background: #fff1f1;
|
||||||
|
color: #b42318;
|
||||||
|
box-shadow: inset 0 0 0 1px #f5c7c4;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.param-token-warning) {
|
||||||
|
background: #fff8e6;
|
||||||
|
color: #92400e;
|
||||||
|
box-shadow: inset 0 0 0 1px #f7d79e;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
[key]: value
|
[key]: value
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
[dataKeyName]: parameters
|
[dataKeyName]: [...parameters]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -172,4 +172,3 @@
|
|||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import NodeWrapper from '../core/NodeWrapper.svelte';
|
import NodeWrapper from '../core/NodeWrapper.svelte';
|
||||||
import {type NodeProps, useSvelteFlow} from '@xyflow/svelte';
|
import {type NodeProps, useNodesData, useSvelteFlow} from '@xyflow/svelte';
|
||||||
import {Button, Heading, Select} from '../base';
|
import {Button, Heading, Select} from '../base';
|
||||||
import {Textarea} from '../base/index.js';
|
|
||||||
import RefParameterList from '../core/RefParameterList.svelte';
|
import RefParameterList from '../core/RefParameterList.svelte';
|
||||||
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
||||||
import {useAddParameter} from '../utils/useAddParameter.svelte';
|
import {useAddParameter} from '../utils/useAddParameter.svelte';
|
||||||
import OutputDefList from '../core/OutputDefList.svelte';
|
import OutputDefList from '../core/OutputDefList.svelte';
|
||||||
// 添加生命周期函数
|
// 添加生命周期函数
|
||||||
import {onMount} from 'svelte';
|
import {onMount} from 'svelte';
|
||||||
|
import ParamTokenEditor from '../core/ParamTokenEditor.svelte';
|
||||||
|
|
||||||
const { data, ...rest }: {
|
const { data, ...rest }: {
|
||||||
data: NodeProps['data'],
|
data: NodeProps['data'],
|
||||||
@@ -25,7 +25,11 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const currentNodeId = getCurrentNodeId();
|
const currentNodeId = getCurrentNodeId();
|
||||||
|
let currentNode = useNodesData(currentNodeId);
|
||||||
const { addParameter } = useAddParameter();
|
const { addParameter } = useAddParameter();
|
||||||
|
const editorParameters = $derived.by(() => {
|
||||||
|
return (currentNode?.current?.data?.parameters as Array<any>) || data.parameters || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const { updateNodeData } = useSvelteFlow();
|
const { updateNodeData } = useSvelteFlow();
|
||||||
@@ -76,15 +80,21 @@
|
|||||||
|
|
||||||
<div class="setting-title">执行代码</div>
|
<div class="setting-title">执行代码</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<Textarea rows={10}
|
<ParamTokenEditor
|
||||||
|
mode="textarea"
|
||||||
|
rows={10}
|
||||||
placeholder="请输入执行代码,注:输出内容需添加到_result中,如:_result['key'] = value 或者 _result.key = value"
|
placeholder="请输入执行代码,注:输出内容需添加到_result中,如:_result['key'] = value 或者 _result.key = value"
|
||||||
style="width: 100%" onchange={(e:any)=>{
|
style="width: 100%"
|
||||||
|
parameters={editorParameters}
|
||||||
|
oninput={(e:any)=>{
|
||||||
updateNodeData(currentNodeId, ()=>{
|
updateNodeData(currentNodeId, ()=>{
|
||||||
return {
|
return {
|
||||||
code: e.target.value
|
code: e.target.value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}} value={data.code as string||""} />
|
}}
|
||||||
|
value={data.code as string||""}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -125,5 +135,3 @@
|
|||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import NodeWrapper from '../core/NodeWrapper.svelte';
|
import NodeWrapper from '../core/NodeWrapper.svelte';
|
||||||
import {type Node, type NodeProps, useSvelteFlow} from '@xyflow/svelte';
|
import {type Node, type NodeProps, useNodesData, useSvelteFlow} from '@xyflow/svelte';
|
||||||
import {Button, Chosen, Heading, Input, Select, Textarea} from '../base';
|
import {Button, Chosen, Heading, Input, Select, Textarea} from '../base';
|
||||||
import RefParameterList from '../core/RefParameterList.svelte';
|
import RefParameterList from '../core/RefParameterList.svelte';
|
||||||
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
import {getOptions} from '../utils/NodeUtils';
|
import {getOptions} from '../utils/NodeUtils';
|
||||||
import OutputDefList from '../core/OutputDefList.svelte';
|
import OutputDefList from '../core/OutputDefList.svelte';
|
||||||
import {fillParameterId} from '../utils/useAddParameter.svelte.js';
|
import {fillParameterId} from '../utils/useAddParameter.svelte.js';
|
||||||
|
import ParamTokenEditor from '../core/ParamTokenEditor.svelte';
|
||||||
|
|
||||||
const { data, ...rest }: {
|
const { data, ...rest }: {
|
||||||
data: NodeProps['data'],
|
data: NodeProps['data'],
|
||||||
@@ -15,9 +16,13 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const currentNodeId = getCurrentNodeId();
|
const currentNodeId = getCurrentNodeId();
|
||||||
|
let currentNode = useNodesData(currentNodeId);
|
||||||
const { addParameter } = useAddParameter();
|
const { addParameter } = useAddParameter();
|
||||||
const flowInstance = useSvelteFlow();
|
const flowInstance = useSvelteFlow();
|
||||||
const { updateNodeData: updateNodeDataInner } = flowInstance;
|
const { updateNodeData: updateNodeDataInner } = flowInstance;
|
||||||
|
const editorParameters = $derived.by(() => {
|
||||||
|
return (currentNode?.current?.data?.parameters as Array<any>) || data.parameters || [];
|
||||||
|
});
|
||||||
|
|
||||||
const updateNodeData = (data: Record<string, any>) => {
|
const updateNodeData = (data: Record<string, any>) => {
|
||||||
updateNodeDataInner(currentNodeId, data);
|
updateNodeDataInner(currentNodeId, data);
|
||||||
@@ -104,19 +109,47 @@
|
|||||||
{#if form.type === 'input'}
|
{#if form.type === 'input'}
|
||||||
<div class="setting-title">{form.label}</div>
|
<div class="setting-title">{form.label}</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<Input
|
{#if form.templateSupport}
|
||||||
placeholder={form.placeholder}
|
<ParamTokenEditor
|
||||||
style="width: 100%"
|
mode="input"
|
||||||
value={data[form.name] || form.defaultValue}
|
placeholder={form.placeholder}
|
||||||
{...form.attrs}
|
style="width: 100%"
|
||||||
onchange={(e)=>{
|
value={data[form.name] || form.defaultValue}
|
||||||
|
parameters={editorParameters}
|
||||||
|
{...form.attrs}
|
||||||
|
oninput={(e)=>{
|
||||||
updateNodeDataByEvent(form.name,e)
|
updateNodeDataByEvent(form.name,e)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{:else}
|
||||||
|
<Input
|
||||||
|
placeholder={form.placeholder}
|
||||||
|
style="width: 100%"
|
||||||
|
value={data[form.name] || form.defaultValue}
|
||||||
|
{...form.attrs}
|
||||||
|
onchange={(e)=>{
|
||||||
|
updateNodeDataByEvent(form.name,e)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if form.type === 'textarea'}
|
{:else if form.type === 'textarea'}
|
||||||
<div class="setting-title">{form.label}</div>
|
<div class="setting-title">{form.label}</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
|
{#if form.templateSupport}
|
||||||
|
<ParamTokenEditor
|
||||||
|
mode="textarea"
|
||||||
|
rows={3}
|
||||||
|
placeholder={form.placeholder}
|
||||||
|
style="width: 100%"
|
||||||
|
value={data[form.name] || form.defaultValue}
|
||||||
|
parameters={editorParameters}
|
||||||
|
{...form.attrs}
|
||||||
|
oninput={(e)=>{
|
||||||
|
updateNodeDataByEvent(form.name,e)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
<Textarea
|
<Textarea
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder={form.placeholder}
|
placeholder={form.placeholder}
|
||||||
@@ -127,6 +160,7 @@
|
|||||||
updateNodeDataByEvent(form.name,e)
|
updateNodeDataByEvent(form.name,e)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if form.type === 'slider'}
|
{:else if form.type === 'slider'}
|
||||||
<div class="setting-title">{form.label}</div>
|
<div class="setting-title">{form.label}</div>
|
||||||
@@ -246,5 +280,3 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import NodeWrapper from '../core/NodeWrapper.svelte';
|
import NodeWrapper from '../core/NodeWrapper.svelte';
|
||||||
import {type NodeProps, useSvelteFlow} from '@xyflow/svelte';
|
import {type NodeProps, useNodesData, useSvelteFlow} from '@xyflow/svelte';
|
||||||
import {Button, Heading, Select} from '../base';
|
import {Button, Heading, Select} from '../base';
|
||||||
import {Input, Textarea} from '../base/index.js';
|
import {Input} from '../base/index.js';
|
||||||
import RefParameterList from '../core/RefParameterList.svelte';
|
import RefParameterList from '../core/RefParameterList.svelte';
|
||||||
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
||||||
import {useAddParameter} from '../utils/useAddParameter.svelte';
|
import {useAddParameter} from '../utils/useAddParameter.svelte';
|
||||||
import OutputDefList from '../core/OutputDefList.svelte';
|
import OutputDefList from '../core/OutputDefList.svelte';
|
||||||
// 添加生命周期函数
|
// 添加生命周期函数
|
||||||
import {onMount} from 'svelte';
|
import {onMount} from 'svelte';
|
||||||
|
import ParamTokenEditor from '../core/ParamTokenEditor.svelte';
|
||||||
|
|
||||||
const { data, ...rest }: {
|
const { data, ...rest }: {
|
||||||
data: NodeProps['data'],
|
data: NodeProps['data'],
|
||||||
@@ -60,10 +61,23 @@
|
|||||||
{ value: 'head', label: 'HEAD' },
|
{ value: 'head', label: 'HEAD' },
|
||||||
{ value: 'patch', label: 'PATCH' }
|
{ value: 'patch', label: 'PATCH' }
|
||||||
];
|
];
|
||||||
|
const bodyTypeOptions = [
|
||||||
|
{ value: '', label: 'none' },
|
||||||
|
{ value: 'form-data', label: 'form-data' },
|
||||||
|
{ value: 'x-www-form-urlencoded', label: 'x-www-form-urlencoded' },
|
||||||
|
{ value: 'json', label: 'json' },
|
||||||
|
{ value: 'raw', label: 'raw' }
|
||||||
|
];
|
||||||
|
|
||||||
const currentNodeId = getCurrentNodeId();
|
const currentNodeId = getCurrentNodeId();
|
||||||
|
let currentNode = useNodesData(currentNodeId);
|
||||||
const { addParameter } = useAddParameter();
|
const { addParameter } = useAddParameter();
|
||||||
const { updateNodeData } = useSvelteFlow();
|
const { updateNodeData } = useSvelteFlow();
|
||||||
|
const editorParameters = $derived.by(() => {
|
||||||
|
return (currentNode?.current?.data?.parameters as Array<any>) || data.parameters || [];
|
||||||
|
});
|
||||||
|
const bodyMethods = new Set(['post', 'put', 'delete', 'patch']);
|
||||||
|
const showBodyConfig = $derived(bodyMethods.has((data.method || '').toLowerCase()));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@@ -89,9 +103,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<RefParameterList />
|
<RefParameterList />
|
||||||
|
|
||||||
<Heading level={3} mt="10px">URL 地址</Heading>
|
<div class="http-section">
|
||||||
<div style="display: flex;gap: 2px;width: 100%;padding: 10px 0">
|
<div class="section-title">请求配置</div>
|
||||||
<div>
|
<div class="http-url-row">
|
||||||
|
<div class="method-select">
|
||||||
<Select items={method} style="width: 100%" placeholder="请选择请求方式" onSelect={(item)=>{
|
<Select items={method} style="width: 100%" placeholder="请选择请求方式" onSelect={(item)=>{
|
||||||
const newValue = item.value;
|
const newValue = item.value;
|
||||||
updateNodeData(currentNodeId, ()=>{
|
updateNodeData(currentNodeId, ()=>{
|
||||||
@@ -100,80 +115,68 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}} value={data.method ? [data.method] : ['get']} />
|
}} value={data.method ? [data.method] : ['get']} />
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 100%">
|
<div class="url-input-wrap">
|
||||||
<Input placeholder="请输入url" style="width: 100%" onchange={(e:any)=>{
|
<Input
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="请输入 URL"
|
||||||
|
value={data.url as string||""}
|
||||||
|
onchange={(e:any)=>{
|
||||||
updateNodeData(currentNodeId, ()=>{
|
updateNodeData(currentNodeId, ()=>{
|
||||||
return {
|
return {
|
||||||
url: e.target.value
|
url: e.target.value
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}} value={data.url as string||""} />
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="heading">
|
<div class="http-section">
|
||||||
<Heading level={3}>Http 头信息</Heading>
|
<div class="heading heading-tight">
|
||||||
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
|
<Heading level={3}>Http 头信息</Heading>
|
||||||
|
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
|
||||||
addParameter(currentNodeId,'headers')
|
addParameter(currentNodeId,'headers')
|
||||||
}}>
|
}}>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
|
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
<RefParameterList dataKeyName="headers" />
|
||||||
</div>
|
</div>
|
||||||
<RefParameterList dataKeyName="headers" />
|
|
||||||
|
|
||||||
{#if data.method === 'post' || data.method === 'put' || data.method === 'delete' || data.method === 'patch'}
|
{#if showBodyConfig}
|
||||||
|
<div class="http-section">
|
||||||
<Heading level={3} mt="10px">Body</Heading>
|
<div class="section-title">Body 类型</div>
|
||||||
<div class="radio-group">
|
<div class="radio-group">
|
||||||
<label><Input type="radio" value="" checked={!data.bodyType || data.bodyType === ''}
|
{#each bodyTypeOptions as option}
|
||||||
onchange={(e:any)=>{
|
<label class:active={((data.bodyType || '') === option.value)}>
|
||||||
if (e.target?.checked){
|
<Input
|
||||||
updateNodeData(currentNodeId,{
|
type="radio"
|
||||||
bodyType: '',
|
name={"http-body-type-" + currentNodeId}
|
||||||
})
|
value={option.value}
|
||||||
}
|
checked={(data.bodyType || '') === option.value}
|
||||||
}} />none</label>
|
onchange={(e:any)=>{
|
||||||
<label><Input type="radio" value="form-data" checked={data.bodyType === 'form-data'}
|
if (e.target?.checked){
|
||||||
onchange={(e:any)=>{
|
updateNodeData(currentNodeId,{
|
||||||
if (e.target?.checked){
|
bodyType: option.value,
|
||||||
updateNodeData(currentNodeId,{
|
})
|
||||||
bodyType: 'form-data',
|
}
|
||||||
})
|
}}
|
||||||
}
|
/>
|
||||||
}} />form-data</label>
|
<span>{option.label}</span>
|
||||||
<label><Input type="radio" value="x-www-form-urlencoded"
|
</label>
|
||||||
checked={data.bodyType === 'x-www-form-urlencoded'} onchange={(e:any)=>{
|
{/each}
|
||||||
if (e.target?.checked){
|
</div>
|
||||||
updateNodeData(currentNodeId,{
|
|
||||||
bodyType: 'x-www-form-urlencoded',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}} />x-www-form-urlencoded</label>
|
|
||||||
<label><Input type="radio" value="json" checked={data.bodyType === 'json'}
|
|
||||||
onchange={(e:any)=>{
|
|
||||||
if (e.target?.checked){
|
|
||||||
updateNodeData(currentNodeId,{
|
|
||||||
bodyType: 'json',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}} />json</label>
|
|
||||||
<label><Input type="radio" value="raw" checked={data.bodyType === 'raw'}
|
|
||||||
onchange={(e:any)=>{
|
|
||||||
if (e.target?.checked){
|
|
||||||
updateNodeData(currentNodeId,{
|
|
||||||
bodyType: 'raw',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}} />raw</label>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
{#if data.bodyType === 'form-data'}
|
{#if data.bodyType === 'form-data'}
|
||||||
<div class="heading" style="padding-top: 10px">
|
<div class="http-section">
|
||||||
|
<div class="heading heading-tight">
|
||||||
<Heading level={3}>参数</Heading>
|
<Heading level={3}>参数</Heading>
|
||||||
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
|
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
|
||||||
addParameter(currentNodeId,'formData')
|
addParameter(currentNodeId,'formData')
|
||||||
@@ -182,13 +185,15 @@
|
|||||||
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
|
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
<RefParameterList dataKeyName="formData" />
|
||||||
</div>
|
</div>
|
||||||
<RefParameterList dataKeyName="formData" />
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
{#if data.bodyType === 'x-www-form-urlencoded'}
|
{#if data.bodyType === 'x-www-form-urlencoded'}
|
||||||
<div class="heading" style="padding-top: 10px">
|
<div class="http-section">
|
||||||
|
<div class="heading heading-tight">
|
||||||
<Heading level={3}>Body 参数</Heading>
|
<Heading level={3}>Body 参数</Heading>
|
||||||
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
|
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
|
||||||
addParameter(currentNodeId,'formUrlencoded')
|
addParameter(currentNodeId,'formUrlencoded')
|
||||||
@@ -197,13 +202,20 @@
|
|||||||
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
|
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
<RefParameterList dataKeyName="formUrlencoded" />
|
||||||
</div>
|
</div>
|
||||||
<RefParameterList dataKeyName="formUrlencoded" />
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if data.bodyType === 'json'}
|
{#if data.bodyType === 'json'}
|
||||||
<div style="width: 100%">
|
<div class="http-section">
|
||||||
<Textarea rows={5} style="width: 100%" placeholder="请输入 json 信息" value={data.bodyJson}
|
<div class="section-title">JSON Body</div>
|
||||||
|
<ParamTokenEditor
|
||||||
|
mode="textarea"
|
||||||
|
rows={5}
|
||||||
|
style="width: 100%"
|
||||||
|
parameters={editorParameters}
|
||||||
|
placeholder="请输入 json 信息" value={data.bodyJson}
|
||||||
oninput={(e:any)=>{
|
oninput={(e:any)=>{
|
||||||
updateNodeData(currentNodeId,{
|
updateNodeData(currentNodeId,{
|
||||||
bodyJson: e.target.value,
|
bodyJson: e.target.value,
|
||||||
@@ -214,8 +226,14 @@
|
|||||||
|
|
||||||
|
|
||||||
{#if data.bodyType === 'raw'}
|
{#if data.bodyType === 'raw'}
|
||||||
<div style="width: 100%">
|
<div class="http-section">
|
||||||
<Textarea rows={5} style="width: 100%" placeholder="请输入请求信息" value={data.bodyRaw}
|
<div class="section-title">Raw Body</div>
|
||||||
|
<ParamTokenEditor
|
||||||
|
mode="textarea"
|
||||||
|
rows={5}
|
||||||
|
style="width: 100%"
|
||||||
|
parameters={editorParameters}
|
||||||
|
placeholder="请输入请求信息" value={data.bodyRaw}
|
||||||
oninput={(e:any)=>{
|
oninput={(e:any)=>{
|
||||||
updateNodeData(currentNodeId,{
|
updateNodeData(currentNodeId,{
|
||||||
bodyRaw: e.target.value,
|
bodyRaw: e.target.value,
|
||||||
@@ -242,22 +260,78 @@
|
|||||||
<style lang="less">
|
<style lang="less">
|
||||||
.heading {
|
.heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.heading-tight {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.http-section {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #e6eaf2;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fafcff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.http-url-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-select {
|
||||||
|
width: 118px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-input-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.radio-group {
|
.radio-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 10px 0;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
font-size: 14px;
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #dbe3f3;
|
||||||
|
background: #fff;
|
||||||
|
color: #4b5563;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
label.active {
|
||||||
|
background: #eef4ff;
|
||||||
|
border-color: #c7d8ff;
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(input[type='radio']) {
|
||||||
|
margin: 0;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import NodeWrapper from '../core/NodeWrapper.svelte';
|
import NodeWrapper from '../core/NodeWrapper.svelte';
|
||||||
import {type NodeProps, useSvelteFlow} from '@xyflow/svelte';
|
import {type NodeProps, useNodesData, useSvelteFlow} from '@xyflow/svelte';
|
||||||
import {Button, Heading, Input, Select} from '../base';
|
import {Button, Heading, Select} from '../base';
|
||||||
import RefParameterList from '../core/RefParameterList.svelte';
|
import RefParameterList from '../core/RefParameterList.svelte';
|
||||||
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
||||||
import {useAddParameter} from '../utils/useAddParameter.svelte';
|
import {useAddParameter} from '../utils/useAddParameter.svelte';
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
import {onMount} from 'svelte';
|
import {onMount} from 'svelte';
|
||||||
import OutputDefList from '../core/OutputDefList.svelte';
|
import OutputDefList from '../core/OutputDefList.svelte';
|
||||||
import type {SelectItem} from '#types';
|
import type {SelectItem} from '#types';
|
||||||
|
import ParamTokenEditor from '../core/ParamTokenEditor.svelte';
|
||||||
|
|
||||||
const { data, ...rest }: {
|
const { data, ...rest }: {
|
||||||
data: NodeProps['data'],
|
data: NodeProps['data'],
|
||||||
@@ -16,7 +17,11 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const currentNodeId = getCurrentNodeId();
|
const currentNodeId = getCurrentNodeId();
|
||||||
|
let currentNode = useNodesData(currentNodeId);
|
||||||
const { addParameter } = useAddParameter();
|
const { addParameter } = useAddParameter();
|
||||||
|
const editorParameters = $derived.by(() => {
|
||||||
|
return (currentNode?.current?.data?.parameters as Array<any>) || data.parameters || [];
|
||||||
|
});
|
||||||
|
|
||||||
const options = getOptions();
|
const options = getOptions();
|
||||||
|
|
||||||
@@ -112,9 +117,13 @@
|
|||||||
|
|
||||||
<div class="setting-title">关键字</div>
|
<div class="setting-title">关键字</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<Input placeholder="请输入关键字" style="width: 100%"
|
<ParamTokenEditor
|
||||||
value={data.keyword}
|
mode="input"
|
||||||
onchange={(e)=>{
|
placeholder="请输入关键字"
|
||||||
|
style="width: 100%"
|
||||||
|
parameters={editorParameters}
|
||||||
|
value={data.keyword || ''}
|
||||||
|
oninput={(e)=>{
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
updateNodeData(currentNodeId, ()=>{
|
updateNodeData(currentNodeId, ()=>{
|
||||||
return {
|
return {
|
||||||
@@ -128,14 +137,21 @@
|
|||||||
|
|
||||||
<div class="setting-title">获取数据量</div>
|
<div class="setting-title">获取数据量</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<Input placeholder="搜索的数据条数" style="width: 100%" onchange={(e)=>{
|
<ParamTokenEditor
|
||||||
|
mode="input"
|
||||||
|
placeholder="搜索的数据条数"
|
||||||
|
style="width: 100%"
|
||||||
|
parameters={editorParameters}
|
||||||
|
oninput={(e)=>{
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
updateNodeData(currentNodeId, ()=>{
|
updateNodeData(currentNodeId, ()=>{
|
||||||
return {
|
return {
|
||||||
limit: newValue
|
limit: newValue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}} value={data.limit || ''} />
|
}}
|
||||||
|
value={data.limit || ''}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -169,5 +185,3 @@
|
|||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import NodeWrapper from '../core/NodeWrapper.svelte';
|
import NodeWrapper from '../core/NodeWrapper.svelte';
|
||||||
import {type NodeProps, useSvelteFlow} from '@xyflow/svelte';
|
import {type NodeProps, useNodesData, useSvelteFlow} from '@xyflow/svelte';
|
||||||
import {Button, FloatingTrigger, Heading, Select} from '../base';
|
import {Button, FloatingTrigger, Heading, Select} from '../base';
|
||||||
import {MenuButton, Textarea} from '../base/index.js';
|
import {MenuButton} from '../base/index.js';
|
||||||
import RefParameterList from '../core/RefParameterList.svelte';
|
import RefParameterList from '../core/RefParameterList.svelte';
|
||||||
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
||||||
import {useAddParameter} from '../utils/useAddParameter.svelte';
|
import {useAddParameter} from '../utils/useAddParameter.svelte';
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
import {onMount} from 'svelte';
|
import {onMount} from 'svelte';
|
||||||
import OutputDefList from '../core/OutputDefList.svelte';
|
import OutputDefList from '../core/OutputDefList.svelte';
|
||||||
import type {SelectItem} from '#types';
|
import type {SelectItem} from '#types';
|
||||||
|
import ParamTokenEditor from '../core/ParamTokenEditor.svelte';
|
||||||
|
|
||||||
const { data, ...rest }: {
|
const { data, ...rest }: {
|
||||||
data: NodeProps['data'],
|
data: NodeProps['data'],
|
||||||
@@ -17,7 +18,11 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const currentNodeId = getCurrentNodeId();
|
const currentNodeId = getCurrentNodeId();
|
||||||
|
let currentNode = useNodesData(currentNodeId);
|
||||||
const { addParameter } = useAddParameter();
|
const { addParameter } = useAddParameter();
|
||||||
|
const editorParameters = $derived.by(() => {
|
||||||
|
return (currentNode?.current?.data?.parameters as Array<any>) || data.parameters || [];
|
||||||
|
});
|
||||||
|
|
||||||
const options = getOptions();
|
const options = getOptions();
|
||||||
|
|
||||||
@@ -177,25 +182,29 @@
|
|||||||
|
|
||||||
<div class="setting-title">系统提示词</div>
|
<div class="setting-title">系统提示词</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<Textarea rows={5}
|
<ParamTokenEditor
|
||||||
|
mode="textarea"
|
||||||
|
rows={5}
|
||||||
placeholder="请输入系统提示词"
|
placeholder="请输入系统提示词"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
parameters={editorParameters}
|
||||||
value={data.systemPrompt || ''}
|
value={data.systemPrompt || ''}
|
||||||
oninput={(e)=>{
|
oninput={(e)=>{
|
||||||
updateNodeData(currentNodeId, {
|
updateNodeData(currentNodeId, {
|
||||||
systemPrompt: e.target.value
|
systemPrompt: e.target.value
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setting-title">用户提示词</div>
|
<div class="setting-title">用户提示词</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<Textarea
|
<ParamTokenEditor
|
||||||
|
mode="textarea"
|
||||||
rows={5}
|
rows={5}
|
||||||
placeholder="请输入用户提示词"
|
placeholder="请输入用户提示词"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
parameters={editorParameters}
|
||||||
value={data.userPrompt || ''}
|
value={data.userPrompt || ''}
|
||||||
oninput={(e)=>{
|
oninput={(e)=>{
|
||||||
updateNodeData(currentNodeId, {
|
updateNodeData(currentNodeId, {
|
||||||
@@ -288,5 +297,3 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import NodeWrapper from '../core/NodeWrapper.svelte';
|
import NodeWrapper from '../core/NodeWrapper.svelte';
|
||||||
import {type NodeProps, useSvelteFlow} from '@xyflow/svelte';
|
import {type NodeProps, useNodesData, useSvelteFlow} from '@xyflow/svelte';
|
||||||
import {Button, Heading, Input, Select} from '../base';
|
import {Button, Heading, Select} from '../base';
|
||||||
import RefParameterList from '../core/RefParameterList.svelte';
|
import RefParameterList from '../core/RefParameterList.svelte';
|
||||||
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
||||||
import {useAddParameter} from '../utils/useAddParameter.svelte';
|
import {useAddParameter} from '../utils/useAddParameter.svelte';
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
import {onMount} from 'svelte';
|
import {onMount} from 'svelte';
|
||||||
import OutputDefList from '../core/OutputDefList.svelte';
|
import OutputDefList from '../core/OutputDefList.svelte';
|
||||||
import type {SelectItem} from '#types';
|
import type {SelectItem} from '#types';
|
||||||
|
import ParamTokenEditor from '../core/ParamTokenEditor.svelte';
|
||||||
|
|
||||||
const { data, ...rest }: {
|
const { data, ...rest }: {
|
||||||
data: NodeProps['data'],
|
data: NodeProps['data'],
|
||||||
@@ -16,7 +17,11 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const currentNodeId = getCurrentNodeId();
|
const currentNodeId = getCurrentNodeId();
|
||||||
|
let currentNode = useNodesData(currentNodeId);
|
||||||
const { addParameter } = useAddParameter();
|
const { addParameter } = useAddParameter();
|
||||||
|
const editorParameters = $derived.by(() => {
|
||||||
|
return (currentNode?.current?.data?.parameters as Array<any>) || data.parameters || [];
|
||||||
|
});
|
||||||
|
|
||||||
const options = getOptions();
|
const options = getOptions();
|
||||||
|
|
||||||
@@ -99,9 +104,13 @@
|
|||||||
|
|
||||||
<div class="setting-title">关键字</div>
|
<div class="setting-title">关键字</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<Input placeholder="请输入关键字" style="width: 100%"
|
<ParamTokenEditor
|
||||||
value={data.keyword}
|
mode="input"
|
||||||
onchange={(e)=>{
|
placeholder="请输入关键字"
|
||||||
|
style="width: 100%"
|
||||||
|
parameters={editorParameters}
|
||||||
|
value={data.keyword || ''}
|
||||||
|
oninput={(e)=>{
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
updateNodeData(currentNodeId, ()=>{
|
updateNodeData(currentNodeId, ()=>{
|
||||||
return {
|
return {
|
||||||
@@ -114,15 +123,21 @@
|
|||||||
|
|
||||||
<div class="setting-title">搜索数据量</div>
|
<div class="setting-title">搜索数据量</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<Input placeholder="搜索的数据条数" style="width: 100%" value={data.limit}
|
<ParamTokenEditor
|
||||||
onchange={(e)=>{
|
mode="input"
|
||||||
|
placeholder="搜索的数据条数"
|
||||||
|
style="width: 100%"
|
||||||
|
value={data.limit || ''}
|
||||||
|
parameters={editorParameters}
|
||||||
|
oninput={(e)=>{
|
||||||
const newValue = e.target.value;
|
const newValue = e.target.value;
|
||||||
updateNodeData(currentNodeId, ()=>{
|
updateNodeData(currentNodeId, ()=>{
|
||||||
return {
|
return {
|
||||||
limit: newValue
|
limit: newValue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -156,5 +171,3 @@
|
|||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
flattenParameterCandidates,
|
||||||
|
findBackspaceTokenRange,
|
||||||
|
findTokenRangeAtCursor,
|
||||||
|
flattenParameterNames,
|
||||||
|
insertTextAtCursor,
|
||||||
|
parseTokenParts,
|
||||||
|
splitTokenDisplay
|
||||||
|
} from './paramToken';
|
||||||
|
|
||||||
|
describe('paramToken utils', () => {
|
||||||
|
it('should flatten parameter names with nested paths', () => {
|
||||||
|
const result = flattenParameterNames([
|
||||||
|
{
|
||||||
|
name: 'input'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'documents',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'title'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'meta',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'author'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
'input',
|
||||||
|
'documents',
|
||||||
|
'documents.title',
|
||||||
|
'documents.meta',
|
||||||
|
'documents.meta.author'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep unresolved candidates in parameter list', () => {
|
||||||
|
const result = flattenParameterCandidates([
|
||||||
|
{
|
||||||
|
name: 'input',
|
||||||
|
refType: 'ref',
|
||||||
|
ref: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'docs',
|
||||||
|
refType: 'ref',
|
||||||
|
ref: 'documents'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'runtimeInput',
|
||||||
|
refType: 'input'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
name: 'input',
|
||||||
|
resolved: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'docs',
|
||||||
|
resolved: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'runtimeInput',
|
||||||
|
resolved: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should insert token text in the middle by cursor range', () => {
|
||||||
|
const result = insertTextAtCursor('hello world', '{{input}}', 6, 11);
|
||||||
|
expect(result).toEqual({
|
||||||
|
value: 'hello {{input}}',
|
||||||
|
cursor: 15
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should append token text when cursor info is missing', () => {
|
||||||
|
const result = insertTextAtCursor('hello', '{{name}}');
|
||||||
|
expect(result).toEqual({
|
||||||
|
value: 'hello{{name}}',
|
||||||
|
cursor: 13
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse token parts and mark valid tokens', () => {
|
||||||
|
const parts = parseTokenParts(
|
||||||
|
'你好 {{ user.name }} 与 {{unknown}}',
|
||||||
|
['user.name', 'docs']
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(parts).toEqual([
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: '你好 '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'token',
|
||||||
|
text: '{{ user.name }}',
|
||||||
|
key: 'user.name',
|
||||||
|
valid: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ' 与 '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'token',
|
||||||
|
text: '{{unknown}}',
|
||||||
|
key: 'unknown',
|
||||||
|
valid: false
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep plain text when token syntax is invalid', () => {
|
||||||
|
const parts = parseTokenParts('abc {{}} def', ['a']);
|
||||||
|
expect(parts).toEqual([
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'abc {{}} def'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should split token display and hide braces text', () => {
|
||||||
|
const result = splitTokenDisplay('{{ user.name }}', 'user.name');
|
||||||
|
expect(result).toEqual({
|
||||||
|
hiddenPrefix: '{{ ',
|
||||||
|
visibleText: 'user.name',
|
||||||
|
hiddenSuffix: ' }}'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find full token range for backspace delete', () => {
|
||||||
|
const content = 'hello {{input}} world';
|
||||||
|
const tokenEndCursor = 'hello {{input}}'.length;
|
||||||
|
const range = findBackspaceTokenRange(content, tokenEndCursor);
|
||||||
|
|
||||||
|
expect(range).toEqual({
|
||||||
|
start: 6,
|
||||||
|
end: 15,
|
||||||
|
text: '{{input}}',
|
||||||
|
key: 'input'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support boundary match for arrow skip behavior', () => {
|
||||||
|
const content = 'x{{docs}}y';
|
||||||
|
const tokenStart = 1;
|
||||||
|
const tokenEnd = 9;
|
||||||
|
|
||||||
|
const rightBoundary = findTokenRangeAtCursor(content, tokenStart, {
|
||||||
|
includeStart: true,
|
||||||
|
includeEnd: false
|
||||||
|
});
|
||||||
|
const leftBoundary = findTokenRangeAtCursor(content, tokenEnd, {
|
||||||
|
includeStart: false,
|
||||||
|
includeEnd: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rightBoundary?.key).toBe('docs');
|
||||||
|
expect(leftBoundary?.key).toBe('docs');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
export interface ParameterLike {
|
||||||
|
name?: string;
|
||||||
|
ref?: string;
|
||||||
|
refType?: string;
|
||||||
|
children?: ParameterLike[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenRange {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
text: string;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TokenPart =
|
||||||
|
| {
|
||||||
|
type: 'text';
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'token';
|
||||||
|
text: string;
|
||||||
|
key: string;
|
||||||
|
valid: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ParameterCandidate {
|
||||||
|
name: string;
|
||||||
|
resolved: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOKEN_PATTERN = /\{\{\s*([^{}]+?)\s*}}/g;
|
||||||
|
|
||||||
|
export function normalizeTokenKey(tokenKey: string): string {
|
||||||
|
return tokenKey.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenParameterNames(parameters?: ParameterLike[] | null): string[] {
|
||||||
|
return flattenParameterCandidates(parameters).map((item) => item.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isParameterResolved(parameter?: ParameterLike): boolean {
|
||||||
|
if (!parameter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refType = (parameter.refType || '').trim();
|
||||||
|
if (refType === 'fixed' || refType === 'input') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ref = (parameter.ref || '').trim();
|
||||||
|
return !!ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenParameterCandidates(parameters?: ParameterLike[] | null): ParameterCandidate[] {
|
||||||
|
if (!parameters || parameters.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates: ParameterCandidate[] = [];
|
||||||
|
const indexMap = new Map<string, number>();
|
||||||
|
|
||||||
|
const addCandidate = (name: string, resolved: boolean) => {
|
||||||
|
const normalized = name.trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const exists = indexMap.get(normalized);
|
||||||
|
if (exists === undefined) {
|
||||||
|
indexMap.set(normalized, candidates.length);
|
||||||
|
candidates.push({
|
||||||
|
name: normalized,
|
||||||
|
resolved
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同名参数只要有一个可解析,就视为可解析
|
||||||
|
if (resolved) {
|
||||||
|
candidates[exists].resolved = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const walk = (items: ParameterLike[], parentPath = '', inheritedResolved = true) => {
|
||||||
|
for (const item of items) {
|
||||||
|
const rawName = item?.name?.trim();
|
||||||
|
if (!rawName) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPath = parentPath ? `${parentPath}.${rawName}` : rawName;
|
||||||
|
const currentResolved = inheritedResolved && isParameterResolved(item);
|
||||||
|
addCandidate(currentPath, currentResolved);
|
||||||
|
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
walk(item.children, currentPath, currentResolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
walk(parameters);
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseTokenParts(content: string, validParams: string[] = []): TokenPart[] {
|
||||||
|
const source = content ?? '';
|
||||||
|
const validSet = new Set(validParams.map(normalizeTokenKey));
|
||||||
|
const parts: TokenPart[] = [];
|
||||||
|
|
||||||
|
let lastIndex = 0;
|
||||||
|
TOKEN_PATTERN.lastIndex = 0;
|
||||||
|
let match: RegExpExecArray | null = TOKEN_PATTERN.exec(source);
|
||||||
|
|
||||||
|
while (match) {
|
||||||
|
if (match.index > lastIndex) {
|
||||||
|
parts.push({
|
||||||
|
type: 'text',
|
||||||
|
text: source.slice(lastIndex, match.index)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawToken = match[0];
|
||||||
|
const tokenKey = normalizeTokenKey(match[1] || '');
|
||||||
|
parts.push({
|
||||||
|
type: 'token',
|
||||||
|
text: rawToken,
|
||||||
|
key: tokenKey,
|
||||||
|
valid: validSet.has(tokenKey)
|
||||||
|
});
|
||||||
|
|
||||||
|
lastIndex = match.index + rawToken.length;
|
||||||
|
match = TOKEN_PATTERN.exec(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastIndex < source.length) {
|
||||||
|
parts.push({
|
||||||
|
type: 'text',
|
||||||
|
text: source.slice(lastIndex)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length === 0) {
|
||||||
|
parts.push({
|
||||||
|
type: 'text',
|
||||||
|
text: source
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTokenRanges(content: string): TokenRange[] {
|
||||||
|
const source = content ?? '';
|
||||||
|
const ranges: TokenRange[] = [];
|
||||||
|
TOKEN_PATTERN.lastIndex = 0;
|
||||||
|
let match: RegExpExecArray | null = TOKEN_PATTERN.exec(source);
|
||||||
|
|
||||||
|
while (match) {
|
||||||
|
const rawToken = match[0];
|
||||||
|
ranges.push({
|
||||||
|
start: match.index,
|
||||||
|
end: match.index + rawToken.length,
|
||||||
|
text: rawToken,
|
||||||
|
key: normalizeTokenKey(match[1] || '')
|
||||||
|
});
|
||||||
|
match = TOKEN_PATTERN.exec(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findTokenRangeAtCursor(
|
||||||
|
content: string,
|
||||||
|
cursor: number,
|
||||||
|
options?: {
|
||||||
|
includeStart?: boolean;
|
||||||
|
includeEnd?: boolean;
|
||||||
|
}
|
||||||
|
): TokenRange | null {
|
||||||
|
if (!Number.isInteger(cursor) || cursor < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const includeStart = options?.includeStart ?? false;
|
||||||
|
const includeEnd = options?.includeEnd ?? false;
|
||||||
|
const ranges = getTokenRanges(content);
|
||||||
|
for (const range of ranges) {
|
||||||
|
const leftValid = includeStart ? cursor >= range.start : cursor > range.start;
|
||||||
|
const rightValid = includeEnd ? cursor <= range.end : cursor < range.end;
|
||||||
|
if (leftValid && rightValid) {
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findBackspaceTokenRange(content: string, cursor: number): TokenRange | null {
|
||||||
|
return findTokenRangeAtCursor(content, cursor, {
|
||||||
|
includeStart: false,
|
||||||
|
includeEnd: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitTokenDisplay(rawToken: string, normalizedKey?: string): {
|
||||||
|
hiddenPrefix: string;
|
||||||
|
visibleText: string;
|
||||||
|
hiddenSuffix: string;
|
||||||
|
} {
|
||||||
|
const source = rawToken ?? '';
|
||||||
|
if (!source.startsWith('{{') || !source.endsWith('}}')) {
|
||||||
|
return {
|
||||||
|
hiddenPrefix: '',
|
||||||
|
visibleText: normalizedKey || source,
|
||||||
|
hiddenSuffix: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const inner = source.slice(2, -2);
|
||||||
|
const visibleText = normalizeTokenKey(normalizedKey || inner);
|
||||||
|
if (!visibleText) {
|
||||||
|
return {
|
||||||
|
hiddenPrefix: '',
|
||||||
|
visibleText: source,
|
||||||
|
hiddenSuffix: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const innerStart = inner.indexOf(visibleText);
|
||||||
|
const leading = innerStart >= 0 ? inner.slice(0, innerStart) : '';
|
||||||
|
const trailing = innerStart >= 0 ? inner.slice(innerStart + visibleText.length) : '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
hiddenPrefix: `{{${leading}`,
|
||||||
|
visibleText,
|
||||||
|
hiddenSuffix: `${trailing}}}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function insertTextAtCursor(
|
||||||
|
content: string,
|
||||||
|
insertedText: string,
|
||||||
|
selectionStart?: number | null,
|
||||||
|
selectionEnd?: number | null
|
||||||
|
): {
|
||||||
|
value: string;
|
||||||
|
cursor: number;
|
||||||
|
} {
|
||||||
|
const source = content ?? '';
|
||||||
|
const start = Number.isInteger(selectionStart)
|
||||||
|
? Math.max(0, Math.min(selectionStart as number, source.length))
|
||||||
|
: source.length;
|
||||||
|
const end = Number.isInteger(selectionEnd)
|
||||||
|
? Math.max(start, Math.min(selectionEnd as number, source.length))
|
||||||
|
: start;
|
||||||
|
|
||||||
|
const nextValue = source.slice(0, start) + insertedText + source.slice(end);
|
||||||
|
return {
|
||||||
|
value: nextValue,
|
||||||
|
cursor: start + insertedText.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function escapeHtml(text: string): string {
|
||||||
|
return text
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ export type CustomNodeForm = {
|
|||||||
defaultValue?: string | number | boolean;
|
defaultValue?: string | number | boolean;
|
||||||
attrs?: Record<string, any>;
|
attrs?: Record<string, any>;
|
||||||
options?: SelectItem[];
|
options?: SelectItem[];
|
||||||
|
templateSupport?: boolean;
|
||||||
chosen?: {
|
chosen?: {
|
||||||
labelDataKey: string;
|
labelDataKey: string;
|
||||||
valueDataKey: string;
|
valueDataKey: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user