mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 01:33:05 +00:00
feat: Support variable splitting nodes (#4181)
This commit is contained in:
parent
cae5682110
commit
112ca7976a
|
|
@ -31,6 +31,7 @@ from .text_to_video_step_node.impl.base_text_to_video_node import BaseTextToVide
|
|||
from .tool_lib_node import *
|
||||
from .tool_node import *
|
||||
from .variable_assign_node import BaseVariableAssignNode
|
||||
from .variable_splitting_node import BaseVariableSplittingNode
|
||||
|
||||
node_list = [BaseStartStepNode, BaseChatNode, BaseSearchKnowledgeNode, BaseQuestionNode,
|
||||
BaseConditionNode, BaseReplyNode,
|
||||
|
|
@ -40,7 +41,7 @@ node_list = [BaseStartStepNode, BaseChatNode, BaseSearchKnowledgeNode, BaseQuest
|
|||
BaseImageGenerateNode, BaseVariableAssignNode, BaseMcpNode, BaseTextToVideoNode, BaseImageToVideoNode,
|
||||
BaseIntentNode, BaseLoopNode, BaseLoopStartStepNode,
|
||||
BaseLoopContinueNode,
|
||||
BaseLoopBreakNode]
|
||||
BaseLoopBreakNode, BaseVariableSplittingNode]
|
||||
|
||||
|
||||
def get_node(node_type):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎虎
|
||||
@file: __init__.py.py
|
||||
@date:2025/10/13 14:56
|
||||
@desc:
|
||||
"""
|
||||
from .impl import *
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# coding=utf-8
|
||||
|
||||
from typing import Type
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from application.flow.i_step_node import INode, NodeResult
|
||||
|
||||
|
||||
class VariableSplittingNodeParamsSerializer(serializers.Serializer):
|
||||
input_variable = serializers.ListField(required=True,
|
||||
label=_("input variable"))
|
||||
|
||||
variable_list = serializers.ListField(required=True,
|
||||
label=_("Split variables"))
|
||||
|
||||
|
||||
class IVariableSplittingNode(INode):
|
||||
type = 'variable-splitting-node'
|
||||
|
||||
def get_node_params_serializer_class(self) -> Type[serializers.Serializer]:
|
||||
return VariableSplittingNodeParamsSerializer
|
||||
|
||||
def _run(self):
|
||||
input_variable = self.workflow_manage.get_reference_field(
|
||||
self.node_params_serializer.data.get('input_variable')[0],
|
||||
self.node_params_serializer.data.get('input_variable')[1:])
|
||||
return self.execute(input_variable, self.node_params_serializer.data['variable_list'])
|
||||
|
||||
def execute(self, input_variable, variable_list, **kwargs) -> NodeResult:
|
||||
pass
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎虎
|
||||
@file: __init__.py.py
|
||||
@date:2025/10/13 15:01
|
||||
@desc:
|
||||
"""
|
||||
from .base_variable_splitting_node import *
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
# coding=utf-8
|
||||
"""
|
||||
@project: MaxKB
|
||||
@Author:虎虎
|
||||
@file: base_variable_splitting_node.py
|
||||
@date:2025/10/13 15:02
|
||||
@desc:
|
||||
"""
|
||||
from jsonpath_ng import parse
|
||||
|
||||
from application.flow.i_step_node import NodeResult
|
||||
from application.flow.step_node.variable_splitting_node.i_variable_splitting_node import IVariableSplittingNode
|
||||
|
||||
|
||||
def smart_jsonpath_search(data: dict, path: str):
|
||||
"""
|
||||
智能JSON Path搜索
|
||||
返回:
|
||||
- 单个匹配: 直接返回值
|
||||
- 多个匹配: 返回值的列表
|
||||
- 无匹配: 返回None
|
||||
"""
|
||||
jsonpath_expr = parse(path)
|
||||
matches = jsonpath_expr.find(data)
|
||||
|
||||
if not matches:
|
||||
return None
|
||||
elif len(matches) == 1:
|
||||
return matches[0].value
|
||||
else:
|
||||
return [match.value for match in matches]
|
||||
|
||||
|
||||
class BaseVariableSplittingNode(IVariableSplittingNode):
|
||||
def save_context(self, details, workflow_manage):
|
||||
for key, value in details.get('result').items():
|
||||
self.context['key'] = value
|
||||
self.context['result'] = details.get('result')
|
||||
|
||||
def execute(self, input_variable, variable_list, **kwargs) -> NodeResult:
|
||||
response = {v['field']: smart_jsonpath_search(input_variable, v['expression']) for v in variable_list}
|
||||
return NodeResult({'result': response, **response}, {})
|
||||
|
||||
def get_details(self, index: int, **kwargs):
|
||||
return {
|
||||
'name': self.node.properties.get('stepName'),
|
||||
"index": index,
|
||||
'run_time': self.context.get('run_time'),
|
||||
'type': self.node.type,
|
||||
'result': self.context.get('result'),
|
||||
'status': self.status,
|
||||
'err_message': self.err_message
|
||||
}
|
||||
|
|
@ -57,6 +57,7 @@ dependencies = [
|
|||
"websockets==15.0.1",
|
||||
"pylint==3.3.7",
|
||||
"cohere==5.17.0",
|
||||
"jsonpath-ng==1.7.0"
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5556_5709)">
|
||||
<path d="M0 10C0 5.28595 0 2.92893 1.46447 1.46447C2.92893 0 5.28595 0 10 0C14.714 0 17.0711 0 18.5355 1.46447C20 2.92893 20 5.28595 20 10C20 14.714 20 17.0711 18.5355 18.5355C17.0711 20 14.714 20 10 20C5.28595 20 2.92893 20 1.46447 18.5355C0 17.0711 0 14.714 0 10Z" fill="#3370FF"/>
|
||||
<path d="M6.60996 5.60895C6.60996 5.88509 6.83382 6.10895 7.10996 6.10895H8.04429L8.07074 6.11149C8.21479 6.1262 8.34803 6.18914 8.45017 6.28798C8.54062 6.37555 8.6022 6.48686 8.62972 6.60689L9.44912 9.63017L9.28076 9.83769L6.91056 12.7516C6.74945 12.951 6.54694 13.1128 6.31649 13.2246C6.27958 13.2425 6.10572 13.2589 5.93452 13.2738C5.71837 13.2927 5.57288 13.4602 5.57288 13.6771V13.7718C5.57288 14.0882 5.86591 14.342 6.17866 14.2943C6.40566 14.2596 6.62664 14.2188 6.7239 14.1717C7.08491 13.9968 7.40483 13.7419 7.66079 13.4245L7.66231 13.423L9.77667 10.8219L10.4175 13.1946L10.418 13.1967C10.513 13.554 10.7189 13.8675 11.0014 14.0903C11.2836 14.3128 11.6274 14.4327 11.98 14.4352L12.8831 14.4329C13.1587 14.4322 13.3818 14.2085 13.3818 13.9329V13.8879C13.3818 13.6118 13.158 13.3879 12.8818 13.3879H11.9541L11.9312 13.3859C11.7875 13.3732 11.654 13.312 11.5508 13.215C11.4588 13.1285 11.3957 13.018 11.3666 12.8981L10.5442 9.87075L10.7125 9.66323L13.0858 6.74778C13.2474 6.54893 13.45 6.38752 13.6804 6.27578C13.7177 6.25768 13.8931 6.24106 14.0652 6.22582C14.2805 6.20676 14.4259 6.04004 14.4262 5.82393C14.4262 5.79061 14.4262 5.75852 14.4263 5.72727C14.4263 5.41181 14.1333 5.15976 13.8215 5.20735C13.5932 5.2422 13.3704 5.28333 13.2725 5.33075C12.9114 5.50563 12.591 5.76054 12.3351 6.07792L12.334 6.07945L10.2207 8.67904L9.57729 6.31189L9.57627 6.30833C9.48171 5.95096 9.27633 5.63745 8.9939 5.41467C8.74689 5.21988 8.4525 5.10334 8.14652 5.07593L8.01479 5.06982H7.10996C6.83382 5.06982 6.60996 5.29368 6.60996 5.56982V5.60895Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5556_5709">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
|
|
@ -32,6 +32,7 @@ export enum WorkflowType {
|
|||
LoopStartNode = 'loop-start-node',
|
||||
LoopContinueNode = 'loop-continue-node',
|
||||
LoopBreakNode = 'loop-break-node',
|
||||
VariableSplittingNode = 'variable-splitting-node',
|
||||
}
|
||||
export enum WorkflowMode {
|
||||
// 应用工作流
|
||||
|
|
|
|||
|
|
@ -396,6 +396,16 @@ You are a master of problem optimization, adept at accurately inferring user int
|
|||
text: 'Terminate the current loop and exit the loop body',
|
||||
isBreak: 'Break',
|
||||
},
|
||||
variableSplittingNode: {
|
||||
label: 'Variable Splitting',
|
||||
text: 'Used to split variables',
|
||||
result: 'Result',
|
||||
splitVariables: 'Split Variables',
|
||||
expression: {
|
||||
label: 'Expression',
|
||||
placeholder: 'Please enter expression',
|
||||
},
|
||||
},
|
||||
},
|
||||
compare: {
|
||||
is_null: 'Is null',
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ export default {
|
|||
placeholder: '请输入关键字搜索',
|
||||
},
|
||||
paramForm: {
|
||||
variable: {
|
||||
label: '变量',
|
||||
},
|
||||
field: {
|
||||
label: '参数',
|
||||
placeholder: '请输入参数',
|
||||
|
|
|
|||
|
|
@ -396,6 +396,16 @@ export default {
|
|||
text: '终止当前循环,跳出循环体',
|
||||
isBreak: 'Break',
|
||||
},
|
||||
variableSplittingNode: {
|
||||
label: '变量拆分',
|
||||
text: '用于拆分变量',
|
||||
result: '结果',
|
||||
splitVariables: '拆分变量',
|
||||
expression: {
|
||||
label: '表达式',
|
||||
placeholder: '请输入表达式',
|
||||
},
|
||||
},
|
||||
},
|
||||
compare: {
|
||||
is_null: '为空',
|
||||
|
|
|
|||
|
|
@ -382,6 +382,16 @@ export default {
|
|||
isContinue: 'Continue',
|
||||
},
|
||||
loopBreakNode: { label: 'Break', text: '終止當前循環,跳出循環體', isBreak: 'Break' },
|
||||
variableSplittingNode: {
|
||||
label: '變量拆分',
|
||||
text: '用於拆分變量',
|
||||
result: '結果',
|
||||
splitVariables: '拆分變量',
|
||||
expression: {
|
||||
label: '表達式',
|
||||
placeholder: '請輸入表達式',
|
||||
},
|
||||
},
|
||||
},
|
||||
compare: {
|
||||
is_null: '為空',
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { h as lh } from '@logicflow/core'
|
|||
import { createApp, h } from 'vue'
|
||||
import directives from '@/directives'
|
||||
import i18n from '@/locales'
|
||||
import { WorkflowType } from '@/enums/application'
|
||||
import { WorkflowMode, WorkflowType } from '@/enums/application'
|
||||
import { nodeDict } from '@/workflow/common/data'
|
||||
import { isActive, connect, disconnect } from './teleport'
|
||||
import { t } from '@/locales'
|
||||
|
|
@ -243,6 +243,7 @@ class AppNode extends HtmlResize.view {
|
|||
return {
|
||||
getNode: () => model,
|
||||
getGraph: () => graphModel,
|
||||
workflowMode: WorkflowMode.Application,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -137,6 +137,24 @@ export const questionNode = {
|
|||
},
|
||||
},
|
||||
}
|
||||
export const variableSplittingNode = {
|
||||
type: WorkflowType.VariableSplittingNode,
|
||||
text: t('views.applicationWorkflow.nodes.variableSplittingNode.text', '变量拆分'),
|
||||
label: t('views.applicationWorkflow.nodes.variableSplittingNode.label', '变量拆分'),
|
||||
height: 345,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.variableSplittingNode.label', '变量拆分'),
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.variableSplittingNode.result', '结果'),
|
||||
value: 'result',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const conditionNode = {
|
||||
type: WorkflowType.Condition,
|
||||
text: t('views.applicationWorkflow.nodes.conditionNode.text'),
|
||||
|
|
@ -540,6 +558,10 @@ export const menuNodes = [
|
|||
label: t('views.applicationWorkflow.nodes.classify.businessLogic'),
|
||||
list: [conditionNode, formNode, variableAssignNode, replyNode, loopNode],
|
||||
},
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.classify.dataProcessing', '数据处理'),
|
||||
list: [variableSplittingNode],
|
||||
},
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.classify.other'),
|
||||
list: [mcpNode, documentExtractNode, toolNode],
|
||||
|
|
@ -565,6 +587,10 @@ export const applicationLoopMenuNodes = [
|
|||
label: t('views.applicationWorkflow.nodes.classify.businessLogic'),
|
||||
list: [conditionNode, formNode, variableAssignNode, replyNode, loopContinueNode, loopBreakNode],
|
||||
},
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.classify.dataProcessing', '数据处理'),
|
||||
list: [variableSplittingNode],
|
||||
},
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.classify.other'),
|
||||
list: [mcpNode, documentExtractNode, toolNode],
|
||||
|
|
@ -666,6 +692,7 @@ export const nodeDict: any = {
|
|||
[WorkflowType.LoopStartNode]: loopStartNode,
|
||||
[WorkflowType.LoopBreakNode]: loopBodyNode,
|
||||
[WorkflowType.LoopContinueNode]: loopContinueNode,
|
||||
[WorkflowType.VariableSplittingNode]: variableSplittingNode,
|
||||
}
|
||||
export function isWorkFlow(type: string | undefined) {
|
||||
return type === 'WORK_FLOW'
|
||||
|
|
|
|||
|
|
@ -39,6 +39,18 @@ export function disconnect(id: string) {
|
|||
delete items[id]
|
||||
}
|
||||
}
|
||||
export function disconnectByFlow(flowId: string) {
|
||||
Object.keys(items).forEach((key) => {
|
||||
if (key.startsWith(flowId)) {
|
||||
delete items[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
export function disconnectAll() {
|
||||
Object.keys(items).forEach((key) => {
|
||||
delete items[key]
|
||||
})
|
||||
}
|
||||
|
||||
export function isActive() {
|
||||
return active
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<el-avatar shape="square" class="avatar-blue">
|
||||
<img src="@/assets/workflow/icon_variable_splitting.svg" style="width: 65%" alt="" />
|
||||
</el-avatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -6,16 +6,16 @@
|
|||
</template>
|
||||
<script setup lang="ts">
|
||||
import LogicFlow from '@logicflow/core'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import AppEdge from './common/edge'
|
||||
import loopEdge from './common/loopEdge'
|
||||
import Control from './common/NodeControl.vue'
|
||||
import { baseNodes } from '@/workflow/common/data'
|
||||
import '@logicflow/extension/lib/style/index.css'
|
||||
import '@logicflow/core/dist/style/index.css'
|
||||
import { initDefaultShortcut } from '@/workflow/common/shortcut'
|
||||
import Dagre from '@/workflow/plugins/dagre'
|
||||
import { getTeleport } from '@/workflow/common/teleport'
|
||||
import { disconnectAll, getTeleport } from '@/workflow/common/teleport'
|
||||
import { WorkflowMode } from '@/enums/application'
|
||||
const nodes: any = import.meta.glob('./nodes/**/index.ts', { eager: true })
|
||||
|
||||
defineOptions({ name: 'WorkFlow' })
|
||||
|
|
@ -40,6 +40,9 @@ const lf = ref()
|
|||
onMounted(() => {
|
||||
renderGraphData()
|
||||
})
|
||||
onUnmounted(() => {
|
||||
disconnectAll()
|
||||
})
|
||||
const render = (data: any) => {
|
||||
lf.value.render(data)
|
||||
}
|
||||
|
|
@ -88,7 +91,13 @@ const renderGraphData = (data?: any) => {
|
|||
lf.value.setDefaultEdgeType('app-edge')
|
||||
|
||||
lf.value.render(data ? data : {})
|
||||
|
||||
lf.value.graphModel.get_provide = (node: any, graph: any) => {
|
||||
return {
|
||||
getNode: () => node,
|
||||
getGraph: () => graph,
|
||||
workflowMode: WorkflowMode.Application,
|
||||
}
|
||||
}
|
||||
lf.value.graphModel.eventCenter.on('delete_edge', (id_list: Array<string>) => {
|
||||
id_list.forEach((id: string) => {
|
||||
lf.value.deleteEdge(id)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<script setup lang="ts">
|
||||
import { set, cloneDeep } from 'lodash'
|
||||
import AppEdge from '@/workflow/common/edge'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import LogicFlow from '@logicflow/core'
|
||||
import Dagre from '@/workflow/plugins/dagre'
|
||||
import { initDefaultShortcut } from '@/workflow/common/shortcut'
|
||||
|
|
@ -14,6 +14,7 @@ import LoopBodyContainer from '@/workflow/nodes/loop-body-node/LoopBodyContainer
|
|||
import { WorkflowMode } from '@/enums/application'
|
||||
import { WorkFlowInstance } from '@/workflow/common/validate'
|
||||
import { t } from '@/locales'
|
||||
import { disconnectByFlow } from '@/workflow/common/teleport'
|
||||
const nodes: any = import.meta.glob('@/workflow/nodes/**/index.ts', { eager: true })
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
const containerRef = ref()
|
||||
|
|
@ -62,7 +63,9 @@ const set_loop_body = () => {
|
|||
const refresh_loop_fields = (fields: Array<any>) => {
|
||||
const loop_node_id = props.nodeModel.properties.loop_node_id
|
||||
const loop_node = props.nodeModel.graphModel.getNodeModelById(loop_node_id)
|
||||
loop_node.properties.config.fields = fields
|
||||
if (loop_node) {
|
||||
loop_node.properties.config.fields = fields
|
||||
}
|
||||
}
|
||||
|
||||
const lf = ref()
|
||||
|
|
@ -158,5 +161,9 @@ onMounted(() => {
|
|||
set(props.nodeModel, 'validate', validate)
|
||||
set(props.nodeModel, 'set_loop_body', set_loop_body)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
disconnectByFlow(lf.value.graphModel.flowId)
|
||||
lf.value = null
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@
|
|||
<script setup lang="ts">
|
||||
import { set, throttle } from 'lodash'
|
||||
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { ref, computed, onMounted, watch, onUnmounted } from 'vue'
|
||||
import { isLastNode } from '@/workflow/common/data'
|
||||
import { loopBodyNode, loopStartNode } from '@/workflow/common/data'
|
||||
import NodeCascader from '@/workflow/common/NodeCascader.vue'
|
||||
|
|
|
|||
|
|
@ -33,7 +33,12 @@
|
|||
{{ item.name }}
|
||||
</auto-tooltip>
|
||||
</div>
|
||||
<el-tooltip v-if="item.desc" effect="dark" placement="right" popper-class="max-w-200">
|
||||
<el-tooltip
|
||||
v-if="item.desc"
|
||||
effect="dark"
|
||||
placement="right"
|
||||
popper-class="max-w-200"
|
||||
>
|
||||
<template #content>
|
||||
{{ item.desc }}
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="isEdit ? $t('common.param.editParam') : $t('common.param.addParam')"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:destroy-on-close="true"
|
||||
:before-close="close"
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
label-position="top"
|
||||
ref="fieldFormRef"
|
||||
:rules="rules"
|
||||
:model="form"
|
||||
require-asterisk-position="right"
|
||||
>
|
||||
<el-form-item
|
||||
:label="$t('dynamicsForm.paramForm.field.label')"
|
||||
:required="true"
|
||||
prop="field"
|
||||
:rules="rules.field"
|
||||
>
|
||||
<el-input
|
||||
v-model="form.field"
|
||||
:maxlength="64"
|
||||
:placeholder="$t('dynamicsForm.paramForm.field.placeholder')"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('dynamicsForm.paramForm.name.label')"
|
||||
:required="true"
|
||||
prop="label"
|
||||
:rules="rules.label"
|
||||
>
|
||||
<el-input
|
||||
v-model="form.label"
|
||||
:maxlength="64"
|
||||
show-word-limit
|
||||
:placeholder="$t('dynamicsForm.paramForm.name.placeholder')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="
|
||||
$t('views.applicationWorkflow.nodes.variableSplittingNode.expression.label', '表达式')
|
||||
"
|
||||
:required="true"
|
||||
prop="label"
|
||||
:rules="rules.label"
|
||||
>
|
||||
<el-input
|
||||
v-model="form.expression"
|
||||
:maxlength="64"
|
||||
show-word-limit
|
||||
:placeholder="
|
||||
$t(
|
||||
'views.applicationWorkflow.nodes.variableSplittingNode.expression.placeholder',
|
||||
'请输入表达式',
|
||||
)
|
||||
"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="close"> {{ $t('common.cancel') }} </el-button>
|
||||
<el-button type="primary" @click="submit(fieldFormRef)" :loading="loading">
|
||||
{{ isEdit ? $t('common.save') : $t('common.add') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { t } from '@/locales'
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const fieldFormRef = ref()
|
||||
const loading = ref<boolean>(false)
|
||||
const isEdit = ref(false)
|
||||
const currentIndex = ref(null)
|
||||
const form = ref<any>({
|
||||
field: '',
|
||||
label: '',
|
||||
expression: '',
|
||||
})
|
||||
|
||||
const rules = reactive({
|
||||
label: [
|
||||
{ required: true, message: t('dynamicsForm.paramForm.name.requiredMessage'), trigger: 'blur' },
|
||||
],
|
||||
field: [
|
||||
{ required: true, message: t('dynamicsForm.paramForm.field.requiredMessage'), trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_]+$/,
|
||||
message: t('dynamicsForm.paramForm.field.requiredMessage2'),
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
|
||||
const open = (row: any, index?: any) => {
|
||||
if (row) {
|
||||
form.value = cloneDeep(row)
|
||||
isEdit.value = true
|
||||
currentIndex.value = index
|
||||
}
|
||||
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
dialogVisible.value = false
|
||||
isEdit.value = false
|
||||
currentIndex.value = null
|
||||
form.value = {
|
||||
field: '',
|
||||
label: '',
|
||||
}
|
||||
}
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
emit('refresh', form.value, currentIndex.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open, close })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
<template>
|
||||
<div class="flex-between mb-16">
|
||||
<h5 class="break-all ellipsis lighter" style="max-width: 80%">
|
||||
{{ $t('views.applicationWorkflow.nodes.variableSplittingNode.splitVariables', '拆分变量') }}
|
||||
</h5>
|
||||
<div>
|
||||
<span class="ml-4">
|
||||
<el-button link type="primary" @click="openAddDialog()">
|
||||
<AppIcon iconName="app-add-outlined" class="mr-4"></AppIcon>
|
||||
{{ $t('common.add') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-table
|
||||
v-if="props.nodeModel.properties.node_data.variable_list?.length > 0"
|
||||
:data="props.nodeModel.properties.node_data.variable_list"
|
||||
class="mb-16"
|
||||
ref="tableRef"
|
||||
row-key="field"
|
||||
>
|
||||
<el-table-column
|
||||
prop="field"
|
||||
:label="$t('dynamicsForm.paramForm.variable.label', '变量')"
|
||||
width="95"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span :title="row.field" class="ellipsis-1">{{ row.field }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="label" :label="$t('dynamicsForm.paramForm.name.label')">
|
||||
<template #default="{ row }">
|
||||
<span>
|
||||
<span :title="row.label" class="ellipsis-1">
|
||||
{{ row.label }}
|
||||
</span></span
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('common.operation')" align="left" width="90">
|
||||
<template #default="{ row, $index }">
|
||||
<span class="mr-4">
|
||||
<el-tooltip effect="dark" :content="$t('common.modify')" placement="top">
|
||||
<el-button type="primary" text @click.stop="openAddDialog(row, $index)">
|
||||
<AppIcon iconName="app-edit"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<el-tooltip effect="dark" :content="$t('common.delete')" placement="top">
|
||||
<el-button type="primary" text @click="deleteField($index)">
|
||||
<AppIcon iconName="app-delete"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<VariableFieldDialog
|
||||
ref="VariableFieldDialogRef"
|
||||
@refresh="refreshFieldList"
|
||||
></VariableFieldDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { set, cloneDeep } from 'lodash'
|
||||
import VariableFieldDialog from './VariableFieldDialog.vue'
|
||||
import { MsgError } from '@/utils/message'
|
||||
import { t } from '@/locales'
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
|
||||
const tableRef = ref()
|
||||
const VariableFieldDialogRef = ref()
|
||||
|
||||
const inputFieldList = ref<any[]>([])
|
||||
|
||||
function openAddDialog(data?: any, index?: any) {
|
||||
VariableFieldDialogRef.value.open(data, index)
|
||||
}
|
||||
|
||||
function deleteField(index: any) {
|
||||
inputFieldList.value.splice(index, 1)
|
||||
const fields = [
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.variableSplittingNode.result', '结果'),
|
||||
value: 'result',
|
||||
},
|
||||
...inputFieldList.value.map((item) => ({ label: item.label, value: item.field })),
|
||||
]
|
||||
set(props.nodeModel.properties.config, 'fields', fields)
|
||||
}
|
||||
|
||||
function refreshFieldList(data: any, index: any) {
|
||||
for (let i = 0; i < inputFieldList.value.length; i++) {
|
||||
if (inputFieldList.value[i].field === data.field && index !== i) {
|
||||
MsgError(t('views.applicationWorkflow.tip.paramErrorMessage') + data.field)
|
||||
return
|
||||
}
|
||||
}
|
||||
if ([undefined, null].includes(index)) {
|
||||
inputFieldList.value.push(data)
|
||||
} else {
|
||||
inputFieldList.value.splice(index, 1, data)
|
||||
}
|
||||
VariableFieldDialogRef.value.close()
|
||||
const fields = [
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.variableSplittingNode.result', '结果'),
|
||||
value: 'result',
|
||||
},
|
||||
...inputFieldList.value.map((item) => ({ label: item.label, value: item.field })),
|
||||
]
|
||||
set(props.nodeModel.properties.config, 'fields', fields)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.nodeModel.properties.node_data.variable_list) {
|
||||
inputFieldList.value = cloneDeep(props.nodeModel.properties.node_data.variable_list)
|
||||
}
|
||||
set(props.nodeModel.properties.node_data, 'variable_list', inputFieldList)
|
||||
const fields = [
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.variableSplittingNode.result', '结果'),
|
||||
value: 'result',
|
||||
},
|
||||
...inputFieldList.value.map((item) => ({ label: item.label, value: item.field })),
|
||||
]
|
||||
set(props.nodeModel.properties.config, 'fields', fields)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import VariableSplittingNodeVue from './index.vue'
|
||||
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
|
||||
|
||||
class VariableSplittingNode extends AppNode {
|
||||
constructor(props: any) {
|
||||
super(props, VariableSplittingNodeVue)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
type: 'variable-splitting-node',
|
||||
model: AppNodeModel,
|
||||
view: VariableSplittingNode,
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<NodeContainer :nodeModel="nodeModel">
|
||||
<el-form
|
||||
@submit.prevent
|
||||
:model="form_data"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
label-width="auto"
|
||||
ref="VariableSplittingRef"
|
||||
hide-required-asterisk
|
||||
>
|
||||
<el-form-item
|
||||
:label="$t('views.applicationWorkflow.nodes.variableSplittingNode.label', '输入变量')"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex-between">
|
||||
<div>
|
||||
{{ $t('views.applicationWorkflow.nodes.variableSplittingNode.label', '输入变量') }}
|
||||
<span class="color-danger">*</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<NodeCascader
|
||||
ref="nodeCascaderRef"
|
||||
:nodeModel="nodeModel"
|
||||
class="w-full"
|
||||
:placeholder="$t('views.applicationWorkflow.variable.placeholder')"
|
||||
v-model="form_data.input_variable"
|
||||
/>
|
||||
</el-form-item>
|
||||
<VariableFieldTable ref="VariableFieldTableRef" :node-model="nodeModel"></VariableFieldTable>
|
||||
</el-form>
|
||||
</NodeContainer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
||||
import NodeCascader from '@/workflow/common/NodeCascader.vue'
|
||||
import VariableFieldTable from '@/workflow/nodes/variable-splitting/component/VariableFieldTable.vue'
|
||||
import { set } from 'lodash'
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
|
||||
const form = {
|
||||
input_variable: [],
|
||||
variable_list: [],
|
||||
}
|
||||
|
||||
const form_data = computed({
|
||||
get: () => {
|
||||
if (props.nodeModel.properties.node_data) {
|
||||
return props.nodeModel.properties.node_data
|
||||
} else {
|
||||
set(props.nodeModel.properties, 'node_data', form)
|
||||
}
|
||||
return props.nodeModel.properties.node_data
|
||||
},
|
||||
set: (value) => {
|
||||
set(props.nodeModel.properties, 'node_data', value)
|
||||
},
|
||||
})
|
||||
const VariableSplittingRef = ref()
|
||||
const validate = async () => {
|
||||
return VariableSplittingRef.value.validate()
|
||||
}
|
||||
onMounted(() => {
|
||||
set(props.nodeModel, 'validate', validate)
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
Loading…
Reference in New Issue