feat: 完成工作流开始节点开场表单
- 增加开始节点 startFormMeta/startFormSchema 配置与运行参数解析 - 统一 Admin/UserCenter 开场表单渲染与文件集合输入 - 补充开始表单校验、引用迁移和前端工具测试
This commit is contained in:
@@ -71,6 +71,47 @@
|
||||
const onRunTest = options.onRunTest;
|
||||
|
||||
const { updateEdgeData } = useUpdateEdgeData();
|
||||
const pendingParentRepairs = new Set<string>();
|
||||
|
||||
function scheduleOrphanParentRepair(nodeId: string) {
|
||||
if (!nodeId || pendingParentRepairs.has(nodeId)) {
|
||||
return;
|
||||
}
|
||||
pendingParentRepairs.add(nodeId);
|
||||
queueMicrotask(() => {
|
||||
pendingParentRepairs.delete(nodeId);
|
||||
const currentNode = getNode(nodeId);
|
||||
const parentId = asString(currentNode?.parentId).trim();
|
||||
if (!currentNode || !parentId || getNode(parentId)) {
|
||||
return;
|
||||
}
|
||||
svelteFlow.updateNode(nodeId, {
|
||||
parentId: undefined
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getResolvedParentId(node: null | Node | undefined) {
|
||||
const parentId = asString(node?.parentId).trim();
|
||||
if (!parentId) {
|
||||
return undefined;
|
||||
}
|
||||
if (getNode(parentId)) {
|
||||
return parentId;
|
||||
}
|
||||
if (node?.id) {
|
||||
scheduleOrphanParentRepair(node.id);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function repairOrphanParentNodes() {
|
||||
store.getNodes().forEach((node) => {
|
||||
if (asString(node.parentId).trim() && !getResolvedParentId(node)) {
|
||||
scheduleOrphanParentRepair(node.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getEventClientPosition(event: any) {
|
||||
if (typeof event?.clientX === 'number' && typeof event?.clientY === 'number') {
|
||||
@@ -353,24 +394,27 @@
|
||||
const isValidConnection = (conn: any) => {
|
||||
const sourceNode = getNode(conn.source)!;
|
||||
const targetNode = getNode(conn.target)!;
|
||||
const sourceParentId = getResolvedParentId(sourceNode);
|
||||
const targetParentId = getResolvedParentId(targetNode);
|
||||
|
||||
// 阻止循环节点连接到父级节点 或者 父级节点连接到子级节点
|
||||
if (conn.sourceHandle === 'loop_handle' || sourceNode.parentId) {
|
||||
if (conn.sourceHandle === 'loop_handle' || sourceParentId) {
|
||||
const edges = svelteFlow.getEdges();
|
||||
for (let edge of edges) {
|
||||
if (edge.target === conn.target) {
|
||||
const edgeSourceNode = getNode(edge.source) as Node;
|
||||
if (conn.sourceHandle === 'loop_handle' && edgeSourceNode.parentId !== sourceNode.id) {
|
||||
const edgeSourceParentId = getResolvedParentId(edgeSourceNode);
|
||||
if (conn.sourceHandle === 'loop_handle' && edgeSourceParentId !== sourceNode.id) {
|
||||
return false;
|
||||
}
|
||||
if (sourceNode.parentId && edgeSourceNode.parentId !== sourceNode.parentId) {
|
||||
if (sourceParentId && edgeSourceParentId !== sourceParentId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!sourceNode.parentId && targetNode.parentId && targetNode.parentId !== sourceNode.id) {
|
||||
if (!sourceParentId && targetParentId && targetParentId !== sourceNode.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -446,12 +490,14 @@
|
||||
closeNodePicker();
|
||||
|
||||
const toNode = state.toNode as Node;
|
||||
if (toNode.parentId) {
|
||||
const targetParentId = getResolvedParentId(toNode);
|
||||
if (targetParentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fromNode = state.fromNode as Node;
|
||||
const fromHande = state.fromHandle as any;
|
||||
const sourceParentId = getResolvedParentId(fromNode);
|
||||
|
||||
const newNode = {
|
||||
position: { ...toNode.position }
|
||||
@@ -459,8 +505,8 @@
|
||||
|
||||
if (fromHande.id === 'loop_handle') {
|
||||
newNode.parentId = fromNode.id;
|
||||
} else if (fromNode.parentId) {
|
||||
newNode.parentId = fromNode.parentId;
|
||||
} else if (sourceParentId) {
|
||||
newNode.parentId = sourceParentId;
|
||||
}
|
||||
|
||||
if (newNode.parentId) {
|
||||
@@ -514,10 +560,11 @@
|
||||
showEdgePanel = false;
|
||||
}
|
||||
const targetNode = getNode(edge.target) as Node;
|
||||
if (targetNode && targetNode.parentId) {
|
||||
const targetParentId = getResolvedParentId(targetNode as Node);
|
||||
if (targetNode && targetParentId) {
|
||||
const nodeEdges = getEdgesByTarget(edge.target);
|
||||
// const loopNode = getNode(targetNode.parentId) as Node;
|
||||
const { x, y } = getNodeRelativePosition(targetNode.parentId);
|
||||
const { x, y } = getNodeRelativePosition(targetParentId);
|
||||
if (nodeEdges.length === 0) {
|
||||
svelteFlow.updateNode(targetNode.id, {
|
||||
parentId: undefined,
|
||||
@@ -543,7 +590,7 @@
|
||||
for (let i = 0; i < nodeEdges.length; i++) {
|
||||
const edge = nodeEdges[i];
|
||||
const sourceNode = getNode(edge.source) as Node;
|
||||
if (sourceNode.parentId || sourceNode.type === 'loopNode') {
|
||||
if (getResolvedParentId(sourceNode) || sourceNode.type === 'loopNode') {
|
||||
hasSameParent = true;
|
||||
break;
|
||||
}
|
||||
@@ -680,6 +727,7 @@
|
||||
|
||||
onMount(() => {
|
||||
store.updateEdges((edges) => edges.map((edge) => ensureEdgeVisualDefaults(edge)));
|
||||
repairOrphanParentNodes();
|
||||
if (!readonly) {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('paste', handleGlobalPaste);
|
||||
|
||||
@@ -4,7 +4,15 @@
|
||||
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
|
||||
import {contentTypes, startFormTypes} from '#consts';
|
||||
import type {Parameter} from '#types';
|
||||
import {store} from '#store/stores.svelte';
|
||||
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
||||
import {
|
||||
renameStartFieldReferencesInNodes,
|
||||
removeStartFormField,
|
||||
START_NODE_TYPE,
|
||||
SYSTEM_START_PARAM_NAME,
|
||||
updateStartFormField,
|
||||
} from '../../utils/workflowNodeFields';
|
||||
|
||||
const { parameter, index }: {
|
||||
parameter: Parameter,
|
||||
@@ -20,10 +28,108 @@
|
||||
...(node?.current?.data?.parameters as Array<Parameter>)[index]
|
||||
};
|
||||
});
|
||||
let isSystemStartParam = $derived.by(() => {
|
||||
return param.systemReserved === true && param.name === SYSTEM_START_PARAM_NAME;
|
||||
});
|
||||
let isStartNodeInputParam = $derived.by(() => {
|
||||
return node?.current?.type === START_NODE_TYPE && param.refType === 'input';
|
||||
});
|
||||
let availableFormTypes = $derived.by(() => {
|
||||
if (isSystemStartParam) {
|
||||
return startFormTypes.filter((item) => item.value === 'input' || item.value === 'textarea');
|
||||
}
|
||||
return startFormTypes;
|
||||
});
|
||||
let displayFormTypeValue = $derived.by(() => {
|
||||
if (isStartNodeInputParam && param.contentType === 'file') {
|
||||
return ['file'];
|
||||
}
|
||||
return param.formType ? [param.formType] : [];
|
||||
});
|
||||
let displayParamName = $derived.by(() => {
|
||||
if (isSystemStartParam) {
|
||||
return '用户问题';
|
||||
}
|
||||
return param.name;
|
||||
});
|
||||
|
||||
const { updateNodeData } = useSvelteFlow();
|
||||
|
||||
const toStartFormFieldType = (value: string) => {
|
||||
if (value === 'input') {
|
||||
return 'text';
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const trimString = (value: unknown) => {
|
||||
return value == null ? '' : String(value).trim();
|
||||
};
|
||||
|
||||
const applyStartFieldPatch = (fieldKey: string, patch: Record<string, any>) => {
|
||||
const currentFieldId = trimString(param.id);
|
||||
store.updateNodes((nodes) => {
|
||||
const edges = store.getEdges();
|
||||
let nextFieldKey = fieldKey;
|
||||
const nextNodes = nodes.map((currentNode) => {
|
||||
if (currentNode.id !== currentNodeId) {
|
||||
return currentNode;
|
||||
}
|
||||
const nextData = updateStartFormField(currentNode.data as Record<string, any>, fieldKey, patch);
|
||||
const nextSchema = Array.isArray(nextData.startFormSchema) ? nextData.startFormSchema : [];
|
||||
const matchedField = nextSchema.find((field) => {
|
||||
return (currentFieldId && trimString(field?.id) === currentFieldId)
|
||||
|| trimString(field?.key) === fieldKey;
|
||||
});
|
||||
nextFieldKey = trimString(matchedField?.key) || fieldKey;
|
||||
return {
|
||||
...currentNode,
|
||||
data: {
|
||||
...((currentNode.data || {}) as Record<string, any>),
|
||||
...nextData
|
||||
}
|
||||
};
|
||||
});
|
||||
return renameStartFieldReferencesInNodes(
|
||||
nextNodes,
|
||||
edges,
|
||||
currentNodeId,
|
||||
fieldKey,
|
||||
nextFieldKey
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const updateParameter = (key: string, value: any) => {
|
||||
if (isStartNodeInputParam) {
|
||||
const fieldKey = param.name || '';
|
||||
if (!fieldKey) {
|
||||
return;
|
||||
}
|
||||
let patch: Record<string, any> = {};
|
||||
if (key === 'name') {
|
||||
patch = { key: value };
|
||||
} else if (key === 'required') {
|
||||
patch = { required: Boolean(value) };
|
||||
} else if (key === 'formType') {
|
||||
patch = { type: toStartFormFieldType(value) };
|
||||
} else if (key === 'formLabel') {
|
||||
patch = { label: value };
|
||||
} else if (key === 'formDescription') {
|
||||
patch = { description: value };
|
||||
} else if (key === 'formPlaceholder') {
|
||||
patch = { placeholder: value };
|
||||
} else if (key === 'enums') {
|
||||
patch = { options: Array.isArray(value) ? value : [] };
|
||||
} else if (key === 'contentType') {
|
||||
patch = { type: value === 'file' ? 'file' : 'text' };
|
||||
}
|
||||
if (Object.keys(patch).length === 0) {
|
||||
return;
|
||||
}
|
||||
applyStartFieldPatch(fieldKey, patch);
|
||||
return;
|
||||
}
|
||||
updateNodeData(currentNodeId, (node) => {
|
||||
let parameters = node.data.parameters as Array<Parameter>;
|
||||
(parameters[index] as any)[key] = value;
|
||||
@@ -51,6 +157,9 @@
|
||||
const updateFormType = (item: any) => {
|
||||
const newValue = item.value;
|
||||
updateParameter('formType', newValue);
|
||||
if (isSystemStartParam) {
|
||||
updateParameter('contentType', 'text');
|
||||
}
|
||||
};
|
||||
|
||||
const updateContentType = (item: any) => {
|
||||
@@ -61,6 +170,13 @@
|
||||
|
||||
let triggerObject: any;
|
||||
const handleDelete = () => {
|
||||
if (isStartNodeInputParam) {
|
||||
updateNodeData(currentNodeId, (node) => {
|
||||
return removeStartFormField(node.data as Record<string, any>, param.name);
|
||||
});
|
||||
triggerObject?.hide();
|
||||
return;
|
||||
}
|
||||
updateNodeData(currentNodeId, (node) => {
|
||||
let parameters = node.data.parameters as Array<Parameter>;
|
||||
parameters.splice(index, 1);
|
||||
@@ -75,7 +191,7 @@
|
||||
|
||||
|
||||
<div class="input-item">
|
||||
<Input style="width: 100%;" value={param.name} placeholder="请输入参数名称"
|
||||
<Input style="width: 100%;" value={displayParamName} placeholder="请输入参数名称"
|
||||
disabled={param.nameDisabled === true}
|
||||
oninput={updateName} />
|
||||
</div>
|
||||
@@ -94,22 +210,21 @@
|
||||
<div class="input-more-setting">
|
||||
{#if param.systemReserved}
|
||||
<div class="input-more-item">
|
||||
系统入口参数,当前不可编辑。
|
||||
系统入口参数,名称和必填规则固定,可调整展示标题、说明、占位符和输入方式。
|
||||
</div>
|
||||
{/if}
|
||||
<div class="input-more-item">
|
||||
数据内容:
|
||||
<Select items={contentTypes} style="width: 100%" defaultValue={["text"]}
|
||||
value={param.contentType ? [param.contentType] : []}
|
||||
disabled={param.systemReserved === true}
|
||||
disabled={param.systemReserved === true || isStartNodeInputParam}
|
||||
onSelect={updateContentType}
|
||||
/>
|
||||
</div>
|
||||
<div class="input-more-item">
|
||||
输入方式:
|
||||
<Select items={startFormTypes} style="width: 100%" defaultValue={["input"]}
|
||||
value={param.formType ? [param.formType] : []}
|
||||
disabled={param.systemReserved === true}
|
||||
<Select items={availableFormTypes} style="width: 100%" defaultValue={["input"]}
|
||||
value={displayFormTypeValue}
|
||||
onSelect={updateFormType}
|
||||
/>
|
||||
</div>
|
||||
@@ -127,21 +242,21 @@
|
||||
数据标题:
|
||||
<Textarea rows={1} style="width: 100%;" onchange={(event)=>{
|
||||
updateParamByEvent('formLabel', event)
|
||||
}} disabled={param.systemReserved === true} value={param.formLabel} />
|
||||
}} value={param.formLabel} />
|
||||
</div>
|
||||
|
||||
<div class="input-more-item">
|
||||
数据描述:
|
||||
<Textarea rows={2} style="width: 100%;" onchange={(event)=>{
|
||||
updateParamByEvent('formDescription', event)
|
||||
}} disabled={param.systemReserved === true} value={param.formDescription} />
|
||||
}} value={param.formDescription} />
|
||||
</div>
|
||||
|
||||
<div class="input-more-item">
|
||||
占位符:
|
||||
<Textarea rows={2} style="width: 100%;" onchange={(event)=>{
|
||||
updateParamByEvent('formPlaceholder', event)
|
||||
}} disabled={param.systemReserved === true} value={param.formPlaceholder} />
|
||||
}} value={param.formPlaceholder} />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
} from '@xyflow/svelte';
|
||||
import {Button, Collapse, FloatingTrigger, Input, Textarea} from '../base';
|
||||
import {type Snippet} from 'svelte';
|
||||
import {onDestroy, onMount} from 'svelte';
|
||||
import {useDeleteNode} from '../utils/useDeleteNode.svelte';
|
||||
import {useCopyNode} from '../utils/useCopyNode.svelte';
|
||||
import {getOptions} from '../utils/NodeUtils';
|
||||
@@ -73,6 +74,38 @@
|
||||
options.onNodeExecute?.(getNode(id)!);
|
||||
};
|
||||
let currentNodeId = getCurrentNodeId();
|
||||
let wrapperElement: HTMLDivElement | null = null;
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
let resizeFrame = 0;
|
||||
|
||||
const scheduleUpdateNodeInternals = () => {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
if (resizeFrame) {
|
||||
cancelAnimationFrame(resizeFrame);
|
||||
}
|
||||
resizeFrame = requestAnimationFrame(() => {
|
||||
updateNodeInternals(id);
|
||||
});
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
scheduleUpdateNodeInternals();
|
||||
if (typeof ResizeObserver !== 'undefined' && wrapperElement) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
scheduleUpdateNodeInternals();
|
||||
});
|
||||
resizeObserver.observe(wrapperElement);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (resizeFrame) {
|
||||
cancelAnimationFrame(resizeFrame);
|
||||
}
|
||||
resizeObserver?.disconnect();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -250,7 +283,7 @@
|
||||
</NodeToolbar>
|
||||
{/if}
|
||||
|
||||
<div class="tf-node-wrapper {wrapperClass}">
|
||||
<div class="tf-node-wrapper {wrapperClass}" bind:this={wrapperElement}>
|
||||
<div class="tf-node-wrapper-body">
|
||||
<Collapse {items} activeKeys={activeKeys} onChange={(_,actionKeys) => {
|
||||
updateNodeData(id, {expand: actionKeys?.includes('key')})
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import NodeWrapper from '../core/NodeWrapper.svelte';
|
||||
import {Heading} from '../base';
|
||||
import {Heading, Input, Textarea} from '../base';
|
||||
import {Button} from '../base/index.js';
|
||||
import {type NodeProps} from '@xyflow/svelte';
|
||||
import DefinedParameterList from '../core/DefinedParameterList.svelte';
|
||||
import {getCurrentNodeId} from '#components/utils/NodeUtils';
|
||||
import {useAddParameter} from '../utils/useAddParameter.svelte';
|
||||
import {useSvelteFlow} from '@xyflow/svelte';
|
||||
import {
|
||||
ensureStartNodeParameters,
|
||||
hasSystemStartParameter,
|
||||
appendStartFormField,
|
||||
isSystemStartParameter,
|
||||
normalizeStartNodeData,
|
||||
normalizeStartFormMeta,
|
||||
} from '../../utils/workflowNodeFields';
|
||||
|
||||
const { data, ...rest }: {
|
||||
@@ -19,25 +19,39 @@
|
||||
} = $props();
|
||||
|
||||
const currentNodeId = getCurrentNodeId();
|
||||
const { addParameter } = useAddParameter();
|
||||
const { updateNodeData } = useSvelteFlow();
|
||||
|
||||
$effect(() => {
|
||||
const currentParameters = (data.parameters as Array<any>) || [];
|
||||
if (!hasSystemStartParameter(currentParameters)) {
|
||||
return;
|
||||
}
|
||||
const parameters = ensureStartNodeParameters(currentParameters);
|
||||
if (JSON.stringify(currentParameters) !== JSON.stringify(parameters)) {
|
||||
updateNodeData(currentNodeId, {
|
||||
parameters
|
||||
});
|
||||
const normalizedData = normalizeStartNodeData((data || {}) as Record<string, any>, {
|
||||
allowLegacyParametersOnly: true
|
||||
});
|
||||
const nextData = {
|
||||
...(data || {}),
|
||||
...normalizedData,
|
||||
};
|
||||
if (JSON.stringify(data || {}) !== JSON.stringify(nextData)) {
|
||||
updateNodeData(currentNodeId, nextData);
|
||||
}
|
||||
});
|
||||
|
||||
const updateStartFormMeta = (key: 'title' | 'description' | 'submitText', value: string) => {
|
||||
updateNodeData(currentNodeId, (node) => {
|
||||
const normalizedMeta = normalizeStartFormMeta(node.data?.startFormMeta as Record<string, any>);
|
||||
return {
|
||||
startFormMeta: {
|
||||
...normalizedMeta,
|
||||
[key]: value,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
let currentParameters = $derived.by(() => {
|
||||
return ((data.parameters as Array<any>) || []);
|
||||
});
|
||||
let startFormMeta = $derived.by(() => {
|
||||
return normalizeStartFormMeta((data.startFormMeta as Record<string, any>) || {});
|
||||
});
|
||||
let systemParameters = $derived.by(() => {
|
||||
return currentParameters.filter((parameter) => isSystemStartParameter(parameter));
|
||||
});
|
||||
@@ -55,11 +69,48 @@
|
||||
d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12C15 13.6569 13.6569 15 12 15Z"></path>
|
||||
</svg>
|
||||
{/snippet}
|
||||
<div class="param-section">
|
||||
<div class="heading">
|
||||
<Heading level={3}>开场表单</Heading>
|
||||
</div>
|
||||
<div class="section-description">配置开始问答的标题、说明和提交按钮文案。</div>
|
||||
<div class="meta-form">
|
||||
<div class="meta-form-item">
|
||||
<div class="meta-label">标题</div>
|
||||
<Input
|
||||
style="width: 100%;"
|
||||
value={startFormMeta.title}
|
||||
placeholder="请输入表单标题"
|
||||
oninput={(event) => updateStartFormMeta('title', (event.target as HTMLInputElement)?.value || '')}
|
||||
/>
|
||||
</div>
|
||||
<div class="meta-form-item">
|
||||
<div class="meta-label">说明</div>
|
||||
<Textarea
|
||||
rows={2}
|
||||
style="width: 100%;"
|
||||
value={startFormMeta.description}
|
||||
placeholder="请输入表单说明"
|
||||
oninput={(event) => updateStartFormMeta('description', (event.target as HTMLTextAreaElement)?.value || '')}
|
||||
/>
|
||||
</div>
|
||||
<div class="meta-form-item">
|
||||
<div class="meta-label">提交按钮</div>
|
||||
<Input
|
||||
style="width: 100%;"
|
||||
value={startFormMeta.submitText}
|
||||
placeholder="请输入按钮文案"
|
||||
oninput={(event) => updateStartFormMeta('submitText', (event.target as HTMLInputElement)?.value || '')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="param-section">
|
||||
<div class="heading">
|
||||
<Heading level={3}>系统入口</Heading>
|
||||
</div>
|
||||
<div class="section-description">固定入口参数,作为工作流默认输入来源。</div>
|
||||
<div class="section-description">固定主问题字段,名称与必填规则固定,可调整展示方式与提示文案。</div>
|
||||
<DefinedParameterList parameters={systemParameters} emptyText="暂无系统入口参数" />
|
||||
</div>
|
||||
|
||||
@@ -67,14 +118,20 @@
|
||||
<div class="heading">
|
||||
<Heading level={3}>自定义参数</Heading>
|
||||
<Button class="input-btn-more" style="margin-left: auto" onclick={()=>{
|
||||
addParameter(currentNodeId, "parameters", {refType: "input", name: "newParam", formType: "input", contentType: "text"});
|
||||
updateNodeData(currentNodeId, (node) => {
|
||||
return appendStartFormField(node.data as Record<string, any>, {
|
||||
label: '新字段',
|
||||
type: 'text',
|
||||
placeholder: '请输入内容',
|
||||
});
|
||||
});
|
||||
}}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path>
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="section-description">这里添加额外输入参数,不影响默认入口参数。</div>
|
||||
<div class="section-description">这里添加额外收集字段,字段顺序按列表顺序保存,首版不提供拖拽排序。</div>
|
||||
<DefinedParameterList parameters={customParameters} emptyText="暂无自定义参数" />
|
||||
</div>
|
||||
</NodeWrapper>
|
||||
@@ -93,6 +150,23 @@
|
||||
border-top: 1px solid var(--tf-border-color);
|
||||
}
|
||||
|
||||
.meta-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.meta-form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-size: 12px;
|
||||
color: var(--tf-text-secondary);
|
||||
}
|
||||
|
||||
.section-description {
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
|
||||
Reference in New Issue
Block a user