From 2809f93f1c365d03c85c0cb26a821e8992e9085d Mon Sep 17 00:00:00 2001 From: wangdan-fit2cloud Date: Sun, 6 Sep 2026 18:01:28 +0800 Subject: [PATCH] feat: variable-aggregation-node --- ui/package.json | 3 +- ui/src/assets/iconfont.js | 2 +- ui/src/components/COMPONENT_README.md | 37 ++- ui/src/components/global/mk-table/index.vue | 39 ++- .../items/JsonInputConstructor.vue | 2 +- .../constructor/items/MultiRowConstructor.vue | 2 +- .../items/MultiSelectConstructor.vue | 2 +- .../items/RadioCardConstructor.vue | 2 +- .../constructor/items/RadioRowConstructor.vue | 2 +- .../items/SingleSelectConstructor.vue | 2 +- ui/src/components/mk-form-list/index.vue | 83 +++++- ui/src/utils/UTILS_README.md | 43 +++ ui/src/utils/use-sortable.ts | 74 +++++ ui/src/workflow-canvas/WORKFLOW_README.md | 16 ++ ui/src/workflow-canvas/core/NodeCascader.vue | 1 + .../core/node-container/index.vue | 7 +- .../nodes/ai-chat-node/index.vue | 7 +- .../nodes/application-node/index.vue | 13 +- .../nodes/condition-node/index.vue | 2 +- .../workflow-canvas/nodes/form-node/index.vue | 57 ++-- .../nodes/image-generate-node/index.vue | 3 +- .../nodes/image-to-video-node/index.vue | 7 +- .../nodes/image-understand-node/index.vue | 5 +- .../nodes/intent-node/index.vue | 5 +- .../nodes/parameter-extraction-node/index.vue | 3 +- .../nodes/question-node/index.vue | 3 +- .../nodes/reply-node/index.vue | 1 - .../component/search-scope/index.vue | 1 - .../nodes/search-knowledge-node/index.vue | 1 - .../nodes/speech-to-text-node/index.vue | 5 +- .../nodes/text-to-speech-node/index.vue | 5 +- .../nodes/text-to-video-node/index.vue | 3 +- .../nodes/tool-custom-node/index.vue | 2 +- .../nodes/tool-lib-node/index.vue | 2 +- .../nodes/tool-workflow-lib-node/index.vue | 1 - .../component/GroupFieldDialog.vue | 98 +++---- .../nodes/variable-aggregation-node/index.vue | 254 ++++++------------ .../nodes/variable-assign-node/index.vue | 47 ++-- .../nodes/variable-splitting-node/index.vue | 2 +- .../nodes/video-understand-node/index.vue | 5 +- 40 files changed, 490 insertions(+), 359 deletions(-) create mode 100644 ui/src/utils/use-sortable.ts diff --git a/ui/package.json b/ui/package.json index 163392a314c..e905d91b632 100644 --- a/ui/package.json +++ b/ui/package.json @@ -47,10 +47,10 @@ "pinia": "^3.0.4", "prettier": "3.8.4", "screenfull": "^6.0.2", - "sortablejs": "^1.15.7", "vue": "^3.5.38", "vue-codemirror": "^6.1.1", "vue-demi": "^0.14.10", + "vue-draggable-plus": "^0.6.1", "vue-router": "^5.1.0" }, "devDependencies": { @@ -59,7 +59,6 @@ "@types/lodash": "^4.17.25", "@types/node": "^24.13.2", "@types/nprogress": "^0.2.3", - "@types/sortablejs": "^1.15.9", "@vitejs/plugin-vue": "^6.0.7", "@vitejs/plugin-vue-jsx": "^5.1.5", "@vue/eslint-config-typescript": "^14.8.0", diff --git a/ui/src/assets/iconfont.js b/ui/src/assets/iconfont.js index 2db31ad9298..314f6cb2149 100644 --- a/ui/src/assets/iconfont.js +++ b/ui/src/assets/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_5200172='',(h=>{var a=(l=(l=document.getElementsByTagName("script"))[l.length-1]).getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var o,v,t,i,c,d=function(a,l){l.parentNode.insertBefore(a,l)};if(a&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}o=function(){var a,l=document.createElement("div");l.innerHTML=h._iconfont_svg_string_5200172,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(a=document.body).firstChild?d(l,a.firstChild):a.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(o,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),o()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(t=o,i=h.document,c=!1,m(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,e())})}function e(){c||(c=!0,t())}function m(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(m,50)}e()}})(window); \ No newline at end of file +window._iconfont_svg_string_5200172='',(h=>{var a=(l=(l=document.getElementsByTagName("script"))[l.length-1]).getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var o,v,t,i,c,d=function(a,l){l.parentNode.insertBefore(a,l)};if(a&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}o=function(){var a,l=document.createElement("div");l.innerHTML=h._iconfont_svg_string_5200172,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(a=document.body).firstChild?d(l,a.firstChild):a.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(o,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),o()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(t=o,i=h.document,c=!1,m(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,e())})}function e(){c||(c=!0,t())}function m(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(m,50)}e()}})(window); \ No newline at end of file diff --git a/ui/src/components/COMPONENT_README.md b/ui/src/components/COMPONENT_README.md index 1a779c6a5d7..595c7b5acac 100644 --- a/ui/src/components/COMPONENT_README.md +++ b/ui/src/components/COMPONENT_README.md @@ -621,6 +621,23 @@ Dialog、Drawer、Popover、嵌套区域等其他大、小表格均禁止开启 传入 `size="small"` 时,组件会为内部 `el-table` 添加 `small` class;组件仅提供该样式钩子, 不内置对应样式。 +行拖拽排序通过 `sortable` 按需开启,开启后可从整行任意位置开始拖动,不添加独立的排序图标列。 +使用 `v-model:data` 接收新顺序;也可使用 `:data` 和 `@update:data` 自行写回。 +`row-key` 支持字段路径或函数,默认为 `id`,排序要求每行键值唯一且 +为字符串或数字。 + +```vue + + + +``` + +`sort-change` 在实际顺序变化后返回 `{ oldIndex, newIndex, data }`,不要在回调里再次移动数组。 +分页场景只调整当前传入页的数据,索引从 `0` 开始;跨页位置、接口保存和失败回滚由业务负责。 +有活动列排序、表头筛选、展开行或树形数据时暂停拖拽,避免显示行与数据索引不一致。 +虚拟表格、跨页和跨列表拖拽不属于该接口。排序工具不会深拷贝普通表格的行对象;工作流使用方应在 +数据写回边界自行 `cloneDeep`,保持 LogicFlow 的可观察树约束。 + 表头需要多选筛选时使用 `MkTableFilter`。`label` 设置表头文案,`options` 接收 `OptionItem[]`,`v-model` 绑定已选值;初始不选择任何选项,确认或重置后通过 `change` 返回筛选值。过长的选项文案会显示省略号,悬停时可查看完整文案。 @@ -872,7 +889,7 @@ import MkSourceCard from '@/components/mk-source-card/index.vue' ### MkFormList -用于多个业务字段组成的动态表单行,只负责重复行布局、添加和删除,不管理业务字段、校验规则或 +用于多个业务字段组成的动态表单行,负责重复行布局、添加、删除和可选排序,不管理业务字段、校验规则或 选项请求。通过 `v-model` 传入行数据,`defaultItem` 创建新行,列表始终至少保留一行。默认插槽 提供 `item`、`index`,业务组件在插槽中继续声明 `el-form-item`、字段路径和校验规则。 @@ -901,6 +918,24 @@ const roleSettings = defineModel<{ roleId: string; workspaceIds: string[] }[]>({ `:show-add-button="false"`。`firstRowHasLabel` 默认为 `true`:第一行删除按钮使用 `mt-8`,后续行 使用 `mt-0.5`;并列表单项没有 label 时传入 `:firstRowHasLabel="false"`。删除成功后通过 `remove(item, index)` 返回被删除的行数据和原索引,业务组件可处理关联状态,不需要再次修改列表。 +增删时使用 Lodash `cloneDeep` 回写独立的行数据,新增行也独立克隆 `defaultItem`,避免共享嵌套 +引用及重复挂载 LogicFlow 的 MobX 可观察对象。调用方应使用业务 ID 识别行,不依赖对象引用保持不变。 + +表单行排序通过 `sortable` 开启,同时必须提供稳定且唯一的 `item-key`(字段名或取键函数)。 +组件自带拖拽手柄,添加按钮位于排序容器之外,不需要业务再包拖拽指令或放置手柄。 +不足两行或键值无效时禁用拖拽。排序与增删一样深拷贝写回, +`sort-change` 返回 `{ oldIndex, newIndex, data }`。校验规则和字段路径继续由默认插槽提供。 + +新行包含业务 ID 时,`default-item` 应传工厂函数,在每次点击添加时生成新 ID;普通无 ID 数据 +仍可传默认对象。 + +```vue + + + +``` ### MkSearchList diff --git a/ui/src/components/global/mk-table/index.vue b/ui/src/components/global/mk-table/index.vue index b3a2996dcd1..2ed75d2a73d 100644 --- a/ui/src/components/global/mk-table/index.vue +++ b/ui/src/components/global/mk-table/index.vue @@ -1,7 +1,9 @@ - diff --git a/ui/src/workflow-canvas/nodes/application-node/index.vue b/ui/src/workflow-canvas/nodes/application-node/index.vue index 3182abec6be..0587d87fb65 100644 --- a/ui/src/workflow-canvas/nodes/application-node/index.vue +++ b/ui/src/workflow-canvas/nodes/application-node/index.vue @@ -169,25 +169,24 @@ onMounted(() => { ref="questionCascaderRef" v-model="formData.question_reference_address" :node-model="model" - class="w-full" placeholder="请选择用户问题" /> - + - + - + - + { :prop="`api_input_field_list.${index}.value`" :rules="{ required: field.is_required, message: `请选择${formatFieldLabel(field.variable)}`, trigger: 'change' }" > - + { :prop="`user_input_field_list.${index}.value`" :rules="{ required: field.required, message: `请选择${formatFieldLabel(field.label)}`, trigger: 'change' }" > - + diff --git a/ui/src/workflow-canvas/nodes/condition-node/index.vue b/ui/src/workflow-canvas/nodes/condition-node/index.vue index 43e19667322..b96e117ba63 100644 --- a/ui/src/workflow-canvas/nodes/condition-node/index.vue +++ b/ui/src/workflow-canvas/nodes/condition-node/index.vue @@ -148,7 +148,7 @@ onBeforeUnmount(() => { :prop="`branch.${branchIndex}.conditions.${conditionIndex}.field`" :rules="{ type: 'array', required: true, message: '请选择变量', trigger: 'change' }" > - + - + - + @@ -101,9 +108,8 @@ diff --git a/ui/src/workflow-canvas/nodes/image-generate-node/index.vue b/ui/src/workflow-canvas/nodes/image-generate-node/index.vue index 736fedc5a92..66af849ff03 100644 --- a/ui/src/workflow-canvas/nodes/image-generate-node/index.vue +++ b/ui/src/workflow-canvas/nodes/image-generate-node/index.vue @@ -114,7 +114,7 @@ onMounted(() => { - + diff --git a/ui/src/workflow-canvas/nodes/image-understand-node/index.vue b/ui/src/workflow-canvas/nodes/image-understand-node/index.vue index 52363fcffd2..4e67e4e5d53 100644 --- a/ui/src/workflow-canvas/nodes/image-understand-node/index.vue +++ b/ui/src/workflow-canvas/nodes/image-understand-node/index.vue @@ -136,7 +136,7 @@ onMounted(() => { - + diff --git a/ui/src/workflow-canvas/nodes/intent-node/index.vue b/ui/src/workflow-canvas/nodes/intent-node/index.vue index a1a461c195a..b784f50d5e9 100644 --- a/ui/src/workflow-canvas/nodes/intent-node/index.vue +++ b/ui/src/workflow-canvas/nodes/intent-node/index.vue @@ -150,7 +150,7 @@ onMounted(() => { - + @@ -168,7 +168,6 @@ onMounted(() => { ref="inputVariableCascaderRef" v-model="formData.input_variable" :node-model="model" - class="w-full" placeholder="请选择变量" /> diff --git a/ui/src/workflow-canvas/nodes/question-node/index.vue b/ui/src/workflow-canvas/nodes/question-node/index.vue index fd121c599e5..e905c86a7ce 100644 --- a/ui/src/workflow-canvas/nodes/question-node/index.vue +++ b/ui/src/workflow-canvas/nodes/question-node/index.vue @@ -116,7 +116,7 @@ onMounted(() => { - + diff --git a/ui/src/workflow-canvas/nodes/tool-workflow-lib-node/index.vue b/ui/src/workflow-canvas/nodes/tool-workflow-lib-node/index.vue index 609274642a7..5acf0cb487f 100644 --- a/ui/src/workflow-canvas/nodes/tool-workflow-lib-node/index.vue +++ b/ui/src/workflow-canvas/nodes/tool-workflow-lib-node/index.vue @@ -188,7 +188,6 @@ onMounted(() => { v-if="field.source === 'reference'" v-model="field.value" :node-model="model" - class="w-full" placeholder="请选择参数" />