workflow底层UI库整合至项目,优化构建逻辑

This commit is contained in:
2026-02-24 11:20:18 +08:00
parent 094b185c49
commit 12accb2575
91 changed files with 6820 additions and 115 deletions

View File

@@ -0,0 +1,178 @@
<script lang="ts">
import {Input, MenuButton, Textarea} from '../base';
import {Button, FloatingTrigger, Select} from '../base/index.js';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
import {useRefOptions} from '../utils/useRefOptions.svelte';
import type {Parameter} from '#types';
import {confirmFormTypes, contentTypes} from '#consts';
const { parameter, index, dataKeyName, useChildrenOnly }: {
parameter: Parameter,
index: number,
dataKeyName: string,
useChildrenOnly?: boolean,
} = $props();
let currentNodeId = getCurrentNodeId();
let node = useNodesData(currentNodeId);
let param = $derived.by(() => {
return {
...parameter,
...(node?.current?.data?.[dataKeyName] as Array<Parameter>)[index]
};
});
const { updateNodeData } = useSvelteFlow();
const updateParam = (key: string, value: any) => {
updateNodeData(currentNodeId, (node) => {
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
parameters[index] = {
...parameters[index],
[key]: value
};
return {
[dataKeyName]: parameters
};
});
};
const updateParamByEvent = (name: string, event: Event) => {
const newValue = (event.target as any).value;
updateParam(name, newValue);
};
const updateRef = (item: any) => {
const newValue = item.value;
updateParam('ref', newValue);
};
const updateFormType = (item: any) => {
const newValue = item.value;
updateParam('formType', newValue);
};
const updateContentType = (item: any) => {
const newValue = item.value;
updateParam('contentType', newValue);
};
// const updateRequired = (item: any) => {
// const newValue = item.target.checked;
// updateParam('required', newValue);
// };
let triggerObject: any;
const handleDelete = () => {
updateNodeData(currentNodeId, (node) => {
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
parameters.splice(index, 1);
return {
[dataKeyName]: [...parameters]
};
});
triggerObject?.hide();
};
let selectItems = useRefOptions(useChildrenOnly);
</script>
<div class="input-item">
<Input style="width: 100%;" value={param.name} placeholder="请输入参数名称"
disabled={param.nameDisabled === true}
oninput={(event)=>updateParamByEvent('name', event)} />
</div>
<div class="input-item">
{#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]}
expandAll
onSelect={updateRef} />
{/if}
</div>
<div class="input-item">
<FloatingTrigger placement="bottom" bind:this={triggerObject}>
<MenuButton />
{#snippet floating()}
<div class="input-more-setting">
<div class="input-more-item">
数据内容:
<Select items={contentTypes} style="width: 100%" defaultValue={["text"]}
value={param.contentType ? [param.contentType] : []}
onSelect={updateContentType}
/>
</div>
<div class="input-more-item">
确认方式:
<Select items={confirmFormTypes} style="width: 100%" defaultValue={["single"]}
value={param.formType ? [param.formType] : []}
onSelect={updateFormType}
/>
</div>
<div class="input-more-item">
数据标题:
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
updateParamByEvent('formLabel', event)
}} value={param.formLabel} />
</div>
<div class="input-more-item">
数据描述:
<Textarea rows={2} style="width: 100%;" onchange={(event)=>{
updateParamByEvent('formDescription', event)
}} value={param.formDescription} />
</div>
<!-- <label class="input-item-inline">-->
<!-- <span>是否必填:</span>-->
<!-- <input type="checkbox" checked={false} onchange={updateRequired} />-->
<!-- </label>-->
<div class="input-more-item">
<Button onclick={handleDelete}>删除</Button>
</div>
</div>
{/snippet}
</FloatingTrigger>
</div>
<style lang="less">
.input-item {
display: flex;
align-items: center;
}
.input-more-setting {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
width: 200px;
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
.input-more-item {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,66 @@
<script lang="ts">
import {useNodesData} from '@xyflow/svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import ConfirmParameterItem from './ConfirmParameterItem.svelte';
const {
noneParameterText = '无确认数据',
dataKeyName = 'parameters',
useChildrenOnly,
}: {
noneParameterText?: string;
dataKeyName?: string;
useChildrenOnly?: boolean,
} = $props();
let currentNodeId = getCurrentNodeId();
let node = useNodesData(currentNodeId);
let parameters = $derived.by(() => {
return [...node?.current?.data?.[dataKeyName] as Array<any> || []];
});
</script>
<div class="input-container">
{#if (parameters.length !== 0)}
<div class="input-header">参数名称</div>
<div class="input-header">参数值</div>
<div class="input-header"></div>
{/if}
{#each parameters as param, index (param.id)}
<ConfirmParameterItem parameter={param} index={index} {dataKeyName} {useChildrenOnly}/>
{:else }
<div class="none-params">{noneParameterText}</div>
{/each}
</div>
<style lang="less">
.input-container {
display: grid;
grid-template-columns: 40% 50% 10%;
row-gap: 5px;
column-gap: 3px;
.none-params {
font-size: 12px;
background: #f8f8f8;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 5px;
width: calc(100% - 5px);
grid-column: 1 / -1; /* 从第一列开始到最后一列结束 */
}
.input-header {
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,184 @@
<script lang="ts">
import {Input, Textarea} from '../base';
import {Button, Checkbox, FloatingTrigger, Select} from '../base/index.js';
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
import {contentTypes, startFormTypes} from '#consts';
import type {Parameter} from '#types';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
const { parameter, index }: {
parameter: Parameter,
index: number
} = $props();
let currentNodeId = getCurrentNodeId();
let node = useNodesData(currentNodeId);
let param = $derived.by(() => {
return {
...parameter,
...(node?.current?.data?.parameters as Array<Parameter>)[index]
};
});
const { updateNodeData } = useSvelteFlow();
const updateParameter = (key: string, value: any) => {
updateNodeData(currentNodeId, (node) => {
let parameters = node.data.parameters as Array<Parameter>;
(parameters[index] as any)[key] = value;
return {
parameters
};
});
};
const updateParamByEvent = (name: string, event: Event) => {
const newValue = (event.target as any).value;
updateParameter(name, newValue);
};
const updateName = (event: Event) => {
const newValue = (event.target as any).value;
updateParameter('name', newValue);
};
const updateRequired = (event: Event) => {
const checked = (event.target as any).checked;
updateParameter('required', checked);
};
const updateFormType = (item: any) => {
const newValue = item.value;
updateParameter('formType', newValue);
};
const updateContentType = (item: any) => {
const newValue = item.value;
updateParameter('contentType', newValue);
};
let triggerObject: any;
const handleDelete = () => {
updateNodeData(currentNodeId, (node) => {
let parameters = node.data.parameters as Array<Parameter>;
parameters.splice(index, 1);
return {
parameters: [...parameters]
};
});
triggerObject?.hide();
};
</script>
<div class="input-item">
<Input style="width: 100%;" value={param.name} placeholder="请输入参数名称"
oninput={updateName} />
</div>
<div class="input-item">
<Checkbox checked={param.required} onchange={updateRequired} />
</div>
<div class="input-item">
<FloatingTrigger placement="bottom" bind:this={triggerObject}>
<Button class="input-btn-more">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M4.5 10.5C3.675 10.5 3 11.175 3 12C3 12.825 3.675 13.5 4.5 13.5C5.325 13.5 6 12.825 6 12C6 11.175 5.325 10.5 4.5 10.5ZM19.5 10.5C18.675 10.5 18 11.175 18 12C18 12.825 18.675 13.5 19.5 13.5C20.325 13.5 21 12.825 21 12C21 11.175 20.325 10.5 19.5 10.5ZM12 10.5C11.175 10.5 10.5 11.175 10.5 12C10.5 12.825 11.175 13.5 12 13.5C12.825 13.5 13.5 12.825 13.5 12C13.5 11.175 12.825 10.5 12 10.5Z"></path>
</svg>
</Button>
{#snippet floating()}
<div class="input-more-setting">
<div class="input-more-item">
数据内容:
<Select items={contentTypes} style="width: 100%" defaultValue={["text"]}
value={param.contentType ? [param.contentType] : []}
onSelect={updateContentType}
/>
</div>
<div class="input-more-item">
输入方式:
<Select items={startFormTypes} style="width: 100%" defaultValue={["input"]}
value={param.formType ? [param.formType] : []}
onSelect={updateFormType}
/>
</div>
{#if param.formType === "radio" || param.formType === "checkbox" || param.formType === "select" }
<div class="input-more-item">
数据选项:
<Textarea rows={3} style="width: 100%;" onchange={(event)=>{
updateParameter('enums', event.target?.value.trim().split("\n"))
}} value={param.enums?.join("\n")} placeholder="一行一个选项" />
</div>
{/if}
<div class="input-more-item">
数据标题:
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
updateParamByEvent('formLabel', event)
}} value={param.formLabel} />
</div>
<div class="input-more-item">
数据描述:
<Textarea rows={2} style="width: 100%;" onchange={(event)=>{
updateParamByEvent('formDescription', event)
}} value={param.formDescription} />
</div>
<div class="input-more-item">
占位符:
<Textarea rows={2} style="width: 100%;" onchange={(event)=>{
updateParamByEvent('formPlaceholder', event)
}} value={param.formPlaceholder} />
</div>
<div class="input-more-item">
<Button onclick={handleDelete}>删除</Button>
</div>
</div>
{/snippet}
</FloatingTrigger>
</div>
<style lang="less">
.input-item {
display: flex;
align-items: center;
}
.input-item-inline {
display: flex;
align-items: center;
font-size: 12px;
color: #666;
}
.input-more-setting {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
width: 200px;
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
.input-more-item {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,56 @@
<script lang="ts">
import DefinedParameterItem from './DefinedParameterItem.svelte';
import {useNodesData} from '@xyflow/svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
let currentNodeId = getCurrentNodeId();
let node = useNodesData(currentNodeId);
let parameters = $derived.by(() => {
return [...node?.current?.data?.parameters as Array<any> || []];
});
</script>
<div class="input-container">
{#if (parameters.length !== 0)}
<div class="input-header">参数名称</div>
<div class="input-header">必填</div>
<div class="input-header"></div>
{/if}
{#each parameters as param, index (param.id)}
<DefinedParameterItem parameter={param} index={index} />
{:else }
<div class="none-params">无输入参数</div>
{/each}
</div>
<style lang="less">
.input-container {
display: grid;
grid-template-columns: 80% 10% 10%;
row-gap: 5px;
column-gap: 3px;
.none-params {
font-size: 12px;
background: #f8f8f8;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 5px;
width: calc(100% - 5px);
grid-column: 1 / -1; /* 从第一列开始到最后一列结束 */
}
.input-header {
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import {Button} from '../base/';
import {type Node} from '@xyflow/svelte';
const { icon, title, type, description, extra }: {
icon: string,
title: string,
type: string,
description: string,
extra?: Partial<Node['data']>,
} = $props();
const onDragStart = (event: DragEvent) => {
if (!event.dataTransfer) {
return null;
}
const node = {
type,
data: {
title,
description,
...extra
}
};
event.dataTransfer.setData('application/tinyflow', JSON.stringify(node));
event.dataTransfer.effectAllowed = 'move';
};
</script>
<Button draggable ondragstart={onDragStart} data-node-type={type}>{@html icon} {title}</Button>

View File

@@ -0,0 +1,299 @@
<script lang="ts">
import {Handle, type NodeProps, NodeToolbar, Position, useSvelteFlow} from '@xyflow/svelte';
import {Button, Collapse, FloatingTrigger, Input, Textarea} from '../base';
import {type Snippet} from 'svelte';
import {useDeleteNode} from '../utils/useDeleteNode.svelte';
import {useCopyNode} from '../utils/useCopyNode.svelte';
import {getOptions} from '../utils/NodeUtils';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
const {
data,
id = '',
icon,
handle,
children,
allowExecute = true,
allowCopy = true,
allowDelete = true,
allowSetting = true,
allowSettingOfCondition = true,
showSourceHandle = true,
showTargetHandle = true,
onCollapse
}: {
data: NodeProps['data'],
id?: NodeProps['id'],
icon?: Snippet,
handle?: Snippet,
children: Snippet,
allowExecute?: boolean,
allowCopy?: boolean,
allowDelete?: boolean,
allowSetting?: boolean,
allowSettingOfCondition?: boolean,
showSourceHandle?: boolean,
showTargetHandle?: boolean,
onCollapse?: (key: string) => void,
} = $props();
let activeKeys = data.expand ? ['key'] : [];
const { updateNodeData, getNode } = useSvelteFlow();
const items = $derived.by(() => {
return [{
key: 'key',
icon,
title: data.title as string,
description: data.description as string,
content: children
}];
});
const { deleteNode } = useDeleteNode();
const { copyNode } = useCopyNode();
const options = getOptions();
const executeNode = () => {
options.onNodeExecute?.(getNode(id)!);
};
let currentNodeId = getCurrentNodeId();
</script>
{#if allowExecute || allowCopy || allowDelete}
<NodeToolbar position={Position.Top} align="start">
<div class="tf-node-toolbar">
{#if allowDelete}
<Button class="tf-node-toolbar-item" onclick={()=>{ deleteNode(id) }}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M17 6H22V8H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V8H2V6H7V3C7 2.44772 7.44772 2 8 2H16C16.5523 2 17 2.44772 17 3V6ZM18 8H6V20H18V8ZM9 11H11V17H9V11ZM13 11H15V17H13V11ZM9 4V6H15V4H9Z"></path>
</svg>
</Button>
{/if}
{#if allowCopy}
<Button class="tf-node-toolbar-item" onclick={()=>{copyNode(id)}}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M6.9998 6V3C6.9998 2.44772 7.44752 2 7.9998 2H19.9998C20.5521 2 20.9998 2.44772 20.9998 3V17C20.9998 17.5523 20.5521 18 19.9998 18H16.9998V20.9991C16.9998 21.5519 16.5499 22 15.993 22H4.00666C3.45059 22 3 21.5554 3 20.9991L3.0026 7.00087C3.0027 6.44811 3.45264 6 4.00942 6H6.9998ZM5.00242 8L5.00019 20H14.9998V8H5.00242ZM8.9998 6H16.9998V16H18.9998V4H8.9998V6Z"></path>
</svg>
</Button>
{/if}
{#if allowExecute}
<Button class="tf-node-toolbar-item" onclick={executeNode}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M8 18.3915V5.60846L18.2264 12L8 18.3915ZM6 3.80421V20.1957C6 20.9812 6.86395 21.46 7.53 21.0437L20.6432 12.848C21.2699 12.4563 21.2699 11.5436 20.6432 11.152L7.53 2.95621C6.86395 2.53993 6 3.01878 6 3.80421Z"></path>
</svg>
</Button>
{/if}
{#if allowSetting}
<FloatingTrigger placement="bottom">
<Button class="tf-node-toolbar-item">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
d="M3.33946 17.0002C2.90721 16.2515 2.58277 15.4702 2.36133 14.6741C3.3338 14.1779 3.99972 13.1668 3.99972 12.0002C3.99972 10.8345 3.3348 9.824 2.36353 9.32741C2.81025 7.71651 3.65857 6.21627 4.86474 4.99001C5.7807 5.58416 6.98935 5.65534 7.99972 5.072C9.01009 4.48866 9.55277 3.40635 9.4962 2.31604C11.1613 1.8846 12.8847 1.90004 14.5031 2.31862C14.4475 3.40806 14.9901 4.48912 15.9997 5.072C17.0101 5.65532 18.2187 5.58416 19.1346 4.99007C19.7133 5.57986 20.2277 6.25151 20.66 7.00021C21.0922 7.7489 21.4167 8.53025 21.6381 9.32628C20.6656 9.82247 19.9997 10.8336 19.9997 12.0002C19.9997 13.166 20.6646 14.1764 21.6359 14.673C21.1892 16.2839 20.3409 17.7841 19.1347 19.0104C18.2187 18.4163 17.0101 18.3451 15.9997 18.9284C14.9893 19.5117 14.4467 20.5941 14.5032 21.6844C12.8382 22.1158 11.1148 22.1004 9.49633 21.6818C9.55191 20.5923 9.00929 19.5113 7.99972 18.9284C6.98938 18.3451 5.78079 18.4162 4.86484 19.0103C4.28617 18.4205 3.77172 17.7489 3.33946 17.0002ZM8.99972 17.1964C10.0911 17.8265 10.8749 18.8227 11.2503 19.9659C11.7486 20.0133 12.2502 20.014 12.7486 19.9675C13.1238 18.8237 13.9078 17.8268 14.9997 17.1964C16.0916 16.5659 17.347 16.3855 18.5252 16.6324C18.8146 16.224 19.0648 15.7892 19.2729 15.334C18.4706 14.4373 17.9997 13.2604 17.9997 12.0002C17.9997 10.74 18.4706 9.5632 19.2729 8.6665C19.1688 8.4405 19.0538 8.21822 18.9279 8.00021C18.802 7.78219 18.667 7.57148 18.5233 7.36842C17.3457 7.61476 16.0911 7.43414 14.9997 6.80405C13.9083 6.17395 13.1246 5.17768 12.7491 4.03455C12.2509 3.98714 11.7492 3.98646 11.2509 4.03292C10.8756 5.17671 10.0916 6.17364 8.99972 6.80405C7.9078 7.43447 6.65245 7.61494 5.47428 7.36803C5.18485 7.77641 4.93463 8.21117 4.72656 8.66637C5.52881 9.56311 5.99972 10.74 5.99972 12.0002C5.99972 13.2604 5.52883 14.4372 4.72656 15.3339C4.83067 15.5599 4.94564 15.7822 5.07152 16.0002C5.19739 16.2182 5.3324 16.4289 5.47612 16.632C6.65377 16.3857 7.90838 16.5663 8.99972 17.1964ZM11.9997 15.0002C10.3429 15.0002 8.99972 13.6571 8.99972 12.0002C8.99972 10.3434 10.3429 9.00021 11.9997 9.00021C13.6566 9.00021 14.9997 10.3434 14.9997 12.0002C14.9997 13.6571 13.6566 15.0002 11.9997 15.0002ZM11.9997 13.0002C12.552 13.0002 12.9997 12.5525 12.9997 12.0002C12.9997 11.4479 12.552 11.0002 11.9997 11.0002C11.4474 11.0002 10.9997 11.4479 10.9997 12.0002C10.9997 12.5525 11.4474 13.0002 11.9997 13.0002Z"></path>
</svg>
</Button>
{#snippet floating()}
<div class="settings">
<div class="input-item">
节点名称:
<Input style="width: 100%;" onchange={(event)=>{
const value = (event.target as any).value;
updateNodeData(currentNodeId,{
title: value
})
}} value={data.title} />
</div>
<div class="input-item">
参数描述:
<Textarea rows={3} style="width: 100%;" onchange={(event)=>{
const value = (event.target as any).value;
updateNodeData(currentNodeId,{
description: value
})
}} value={data.description} />
</div>
{#if allowSettingOfCondition}
<div class="input-item">
执行条件:
<Textarea rows={2} style="width: 100%;" onchange={(event)=>{
const value = (event.target as any).value;
updateNodeData(currentNodeId,{
condition: value
})
}} value={data.condition} />
</div>
{/if}
<label class="input-item-inline">
<span>异步执行:</span>
<input type="checkbox" checked={!!data.async} onchange={(event)=>{
const value = (event.target as any).checked;
updateNodeData(currentNodeId,{
async: value
})
}} />
</label>
<label class="input-item-inline">
<span>循环执行:</span>
<input type="checkbox" checked={!!data.loopEnable} onchange={(event)=>{
const value = (event.target as any).checked;
updateNodeData(currentNodeId,{
loopEnable: value
})
}} />
</label>
{#if !!data.loopEnable}
<div class="input-item">
循环间隔时间(单位:毫秒):
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
const value = (event.target as any).value;
updateNodeData(currentNodeId,{
loopIntervalMs: value
})
}} value={data.loopIntervalMs || '1000'} />
</div>
<div class="input-item">
最大循环次数0 表示不限制):
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
const value = (event.target as any).value;
updateNodeData(currentNodeId,{
maxLoopCount: value
})
}} value={data.maxLoopCount || '0'} />
</div>
<div class="input-item">
退出条件:
<Textarea rows={2} style="width: 100%;" onchange={(event)=>{
const value = (event.target as any).value;
updateNodeData(currentNodeId,{
loopBreakCondition: value
})
}} value={data.loopBreakCondition} />
</div>
{/if}
<label class="input-item-inline">
<span>错误重试:</span>
<input type="checkbox" checked={!!data.retryEnable} onchange={(event)=>{
const value = (event.target as any).checked;
updateNodeData(currentNodeId,{
retryEnable: value
})
}} />
</label>
{#if !!data.retryEnable}
<div class="input-item">
错误重试间隔时间(单位:毫秒):
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
const value = (event.target as any).value;
updateNodeData(currentNodeId,{
retryIntervalMs: value
})
}} value={data.retryIntervalMs || '1000'} />
</div>
<div class="input-item">
最大重试次数:
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
const value = (event.target as any).value;
updateNodeData(currentNodeId,{
maxRetryCount: value
})
}} value={data.maxRetryCount || '3'} />
</div>
<label class="input-item-inline">
<span>正常后重置重试次数记录:</span>
<input type="checkbox" checked={!!data.resetRetryCountAfterNormal} onchange={(event)=>{
const value = (event.target as any).checked;
updateNodeData(currentNodeId,{
resetRetryCountAfterNormal: value
})
}} />
</label>
{/if}
</div>
{/snippet}
</FloatingTrigger>
{/if}
</div>
</NodeToolbar>
{/if}
<div class="tf-node-wrapper">
<div class="tf-node-wrapper-body">
<Collapse {items} activeKeys={activeKeys} onChange={(_,actionKeys) => {
updateNodeData(id, {expand: actionKeys?.includes('key')})
onCollapse?.(actionKeys?.includes('key') ? 'key' : '')
}} />
</div>
</div>
{#if showTargetHandle}
<Handle type="target" position={Position.Left} style=" left: -12px;top: 20px" />
{/if}
{#if showSourceHandle}
<Handle type="source" position={Position.Right} style="right: -12px;top: 20px" />
{/if}
{@render handle?.()}
<style lang="less">
.tf-node-toolbar {
display: flex;
gap: 5px;
padding: 5px;
border-radius: 5px;
background: #fff;
border: 1px solid #eee;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
:global(.tf-node-toolbar-item) {
border: 1px solid transparent;
}
.settings {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
width: 200px;
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
.input-item {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 12px;
color: #666;
}
.input-item-inline {
display: flex;
align-items: center;
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,225 @@
<script lang="ts">
import {Input, MenuButton, Textarea} from '../base';
import {Button, FloatingTrigger, Select} from '../base/index.js';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
import {parameterDataTypes} from '#consts';
import {genShortId} from '../utils/IdGen';
import type {Parameter} from '#types';
import {deepClone} from '../utils/deepClone';
const { parameter, position, dataKeyName, placeholder = '请输入参数值' }: {
parameter: Parameter,
position: number[],
dataKeyName: string,
placeholder?: string,
} = $props();
let currentNodeId = getCurrentNodeId();
let node = useNodesData(currentNodeId);
let currentParameter = $derived.by(() => {
let parameters = node?.current?.data?.[dataKeyName] as Parameter[];
let param;
if (parameters && position.length > 0) {
let params = parameters as Array<Parameter>;
for (let i = 0; i < position.length; i++) {
const pos = position[i];
if (i == position.length - 1) {
param = params[pos];
} else {
params = params[pos].children!;
}
}
}
return {
...parameter,
...param
};
});
const { updateNodeData } = useSvelteFlow();
const updateAttribute = (key: string, value: any) => {
updateNodeData(currentNodeId, (node) => {
const parameters = node.data?.[dataKeyName] as Array<Parameter>;
if (parameters && position.length > 0) {
let params = parameters as Parameter[];
for (let i = 0; i < position.length; i++) {
const pos = position[i];
if (i == position.length - 1) {
params[pos] = {
...params[pos],
[key]: value
};
} else {
params = params[pos].children!;
}
}
}
return {
[dataKeyName]: [...deepClone(parameters)]
};
});
};
const updateByEvent = (name: string, event: Event) => {
const newValue = (event.target as any).value;
updateAttribute(name, newValue);
};
const updateDataType = (item: any) => {
const newValue = item.value;
updateAttribute('dataType', newValue);
};
let triggerObject: any;
const handleDelete = () => {
updateNodeData(currentNodeId, (node) => {
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
if (parameters && position.length > 0) {
let params = parameters as Array<Parameter>;
for (let i = 0; i < position.length; i++) {
const pos = position[i];
if (i == position.length - 1) {
params.splice(pos, 1);
} else {
params = params[pos].children!;
}
}
}
return {
[dataKeyName]: [...deepClone(parameters)]
};
});
triggerObject?.hide();
};
const handleAddChildParameter = () => {
updateNodeData(currentNodeId, (node) => {
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
if (parameters && position.length > 0) {
let params = parameters as Array<Parameter>;
for (let i = 0; i < position.length; i++) {
const pos = position[i];
if (i == position.length - 1) {
if (params[pos].children) {
params[pos].children.push({
id: genShortId(),
name: 'newParam',
dataType: 'String'
});
} else {
params[pos].children = [
{
id: genShortId(),
name: 'newParam',
dataType: 'String'
}
];
}
} else {
params = params[pos].children!;
}
}
}
return {
[dataKeyName]: [...deepClone(parameters)]
};
});
};
</script>
<div class="input-item">
{#if (position.length > 1)}
{#each position as p} &nbsp;{/each}
{/if}
<Input style="width: 100%;" value={currentParameter.name} placeholder={placeholder}
oninput={(e)=>{updateByEvent('name',e)}} disabled={currentParameter.nameDisabled === true} />
</div>
<div class="input-item">
<Select items={currentParameter.dataTypeItems || parameterDataTypes} style="width: 100%" defaultValue={["String"]}
value={currentParameter.dataType ? [currentParameter.dataType]:[]}
disabled={currentParameter.dataTypeDisabled === true}
onSelect={updateDataType} />
{#if (currentParameter.dataType === "Object" || currentParameter.dataType === "Array") && currentParameter.addChildDisabled !== true}
<Button class="input-btn-more" style="margin-left: auto" onclick={handleAddChildParameter}>
<svg style="transform: scaleY(-1)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="currentColor">
<path
d="M13 8V16C13 17.6569 11.6569 19 10 19H7.82929C7.41746 20.1652 6.30622 21 5 21C3.34315 21 2 19.6569 2 18C2 16.3431 3.34315 15 5 15C6.30622 15 7.41746 15.8348 7.82929 17H10C10.5523 17 11 16.5523 11 16V8C11 6.34315 12.3431 5 14 5H17V2L22 6L17 10V7H14C13.4477 7 13 7.44772 13 8ZM5 19C5.55228 19 6 18.5523 6 18C6 17.4477 5.55228 17 5 17C4.44772 17 4 17.4477 4 18C4 18.5523 4.44772 19 5 19Z"></path>
</svg>
</Button>
{/if}
</div>
<div class="input-item">
<FloatingTrigger placement="bottom" bind:this={triggerObject}>
<MenuButton />
{#snippet floating()}
<div class="input-more-setting">
<div class="input-more-item">
默认值:
<Textarea rows={1} style="width: 100%;"
value={currentParameter.defaultValue||''}
onchange={(event)=>{
updateByEvent( 'defaultValue', event)
}} />
</div>
<div class="input-more-item">
参数描述:
<Textarea rows={3} style="width: 100%;"
value={currentParameter.description||''}
onchange={(event)=>{
updateByEvent( 'description', event)
}} />
</div>
{#if currentParameter.deleteDisabled !== true}
<div class="input-more-item">
<Button onclick={handleDelete}>删除</Button>
</div>
{/if}
</div>
{/snippet}
</FloatingTrigger>
</div>
<style lang="less">
.input-item {
display: flex;
align-items: center;
gap: 2px;
}
.input-more-setting {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
width: 200px;
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
.input-more-item {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,76 @@
<script lang="ts">
import {useNodesData} from '@xyflow/svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import type {Parameter} from '#types';
import OutputDefItem from './OutputDefItem.svelte';
const {
noneParameterText = '无输出参数',
dataKeyName = 'outputDefs',
placeholder = '请输入参数名称',
}: {
noneParameterText?: string;
dataKeyName?: string;
placeholder?: string
} = $props();
let currentNodeId = getCurrentNodeId();
let node = useNodesData(currentNodeId);
let parameters = $derived.by(() => {
return [...node?.current?.data?.[dataKeyName] as Array<Parameter> || []];
});
</script>
{#snippet parameterList(params: Parameter[], position: number[])}
{#each params as param, index (`${param.id}_${param.children ? param.children.length : 0}`)}
<OutputDefItem parameter={param} position={[...position, index]} {dataKeyName} {placeholder} />
{#if param.children}
{@render parameterList(param.children, [...position, index])}
{/if}
{:else }
{#if position.length === 0}
<div class="none-params">{noneParameterText}</div>
{/if}
{/each}
{/snippet}
<div class="input-container">
{#if (parameters.length !== 0)}
<div class="input-header">参数名称</div>
<div class="input-header">参数类型</div>
<div class="input-header"></div>
{/if}
{@render parameterList(parameters || [], [])}
</div>
<style lang="less">
.input-container {
display: grid;
grid-template-columns: 40% 50% 10%;
row-gap: 5px;
column-gap: 3px;
.none-params {
font-size: 12px;
background: #f8f8f8;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 5px;
width: calc(100% - 5px);
grid-column: 1 / -1; /* 从第一列开始到最后一列结束 */
}
.input-header {
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,175 @@
<script lang="ts">
import {Input, MenuButton, Textarea} from '../base';
import {Button, FloatingTrigger, Select} from '../base/index.js';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
import {contentTypes, parameterRefTypes} from '#consts';
import {useRefOptions} from '../utils/useRefOptions.svelte';
import {onMount} from 'svelte';
import type {Parameter} from '#types';
onMount(() => {
if (!param.refType) {
updateRefType({ value: 'ref' }); // 设置数据来源默认值
}
});
const { parameter, index, dataKeyName, useChildrenOnly, showContentType = false }: {
parameter: Parameter,
index: number,
dataKeyName: string,
useChildrenOnly?: boolean,
showContentType?: boolean,
} = $props();
let currentNodeId = getCurrentNodeId();
let node = useNodesData(currentNodeId);
let param = $derived.by(() => {
return {
...parameter,
...(node?.current?.data?.[dataKeyName] as Array<Parameter>)[index]
};
});
const { updateNodeData } = useSvelteFlow();
const updateParam = (key: string, value: any) => {
updateNodeData(currentNodeId, (node) => {
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
parameters[index] = {
...parameters[index],
[key]: value
};
return {
[dataKeyName]: parameters
};
});
};
const updateParamByEvent = (name: string, event: Event) => {
const newValue = (event.target as any).value;
updateParam(name, newValue);
};
const updateRef = (item: any) => {
const newValue = item.value;
updateParam('ref', newValue);
};
const updateRefType = (item: any) => {
const newValue = item.value;
updateParam('refType', newValue);
};
const updateContentType = (item: any) => {
const newValue = item.value;
updateParam('contentType', newValue);
};
let triggerObject: any;
const handleDelete = () => {
updateNodeData(currentNodeId, (node) => {
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
parameters.splice(index, 1);
return {
[dataKeyName]: [...parameters]
};
});
triggerObject?.hide();
};
let selectItems = useRefOptions(useChildrenOnly);
</script>
<div class="input-item">
<Input style="width: 100%;" value={param.name} placeholder="请输入参数名称"
disabled={param.nameDisabled === true}
oninput={(event)=>updateParamByEvent('name', event)} />
</div>
<div class="input-item">
{#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]}
expandAll
onSelect={updateRef} />
{/if}
</div>
<div class="input-item">
<FloatingTrigger placement="bottom" bind:this={triggerObject}>
<MenuButton />
{#snippet floating()}
<div class="input-more-setting">
<div class="input-more-item">
数据来源:
<Select items={parameterRefTypes} style="width: 100%" defaultValue={["ref"]}
value={param.refType ? [param.refType] : []}
onSelect={updateRefType}
/>
</div>
{#if showContentType }
<div class="input-more-item">
数据内容:
<Select items={contentTypes} style="width: 100%" defaultValue={["text"]}
value={param.contentType ? [param.contentType] : []}
onSelect={updateContentType}
/>
</div>
{/if}
<div class="input-more-item">
默认值:
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
updateParamByEvent('defaultValue', event)
}} value={param.defaultValue} placeholder="请输入参数默认值" />
</div>
<div class="input-more-item">
参数描述:
<Textarea rows={3} style="width: 100%;" onchange={(event)=>{
updateParamByEvent('description', event)
}} value={param.description} placeholder="请输入参数描述"/>
</div>
<div class="input-more-item">
<Button onclick={handleDelete}>删除</Button>
</div>
</div>
{/snippet}
</FloatingTrigger>
</div>
<style lang="less">
.input-item {
display: flex;
align-items: center;
}
.input-more-setting {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
width: 200px;
box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.1);
.input-more-item {
display: flex;
flex-direction: column;
gap: 3px;
font-size: 12px;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,68 @@
<script lang="ts">
import {useNodesData} from '@xyflow/svelte';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import RefParameterItem from './RefParameterItem.svelte';
const {
noneParameterText = '无输入参数',
dataKeyName = 'parameters',
useChildrenOnly,
showContentType = false
}: {
noneParameterText?: string;
dataKeyName?: string;
useChildrenOnly?: boolean,
showContentType?: boolean,
} = $props();
let currentNodeId = getCurrentNodeId();
let node = useNodesData(currentNodeId);
let parameters = $derived.by(() => {
return [...node?.current?.data?.[dataKeyName] as Array<any> || []];
});
</script>
<div class="input-container">
{#if (parameters.length !== 0)}
<div class="input-header">参数名称</div>
<div class="input-header">参数值</div>
<div class="input-header"></div>
{/if}
{#each parameters as param, index (param.id)}
<RefParameterItem parameter={param} index={index} {dataKeyName} {useChildrenOnly} {showContentType} />
{:else }
<div class="none-params">{noneParameterText}</div>
{/each}
</div>
<style lang="less">
.input-container {
display: grid;
grid-template-columns: 40% 50% 10%;
row-gap: 5px;
column-gap: 3px;
.none-params {
font-size: 12px;
background: #f8f8f8;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 5px;
width: calc(100% - 5px);
grid-column: 1 / -1; /* 从第一列开始到最后一列结束 */
}
.input-header {
font-size: 12px;
color: #666;
}
}
</style>