首页工作流引用功能收口:Schema 移除对象/数组引用,模型参数引用与表单展示同级
- FieldConfigDialog(json-schema-editor):删除对象/数组字段的「值来源」引用入口, 标量字段的默认值引用保留 - ModelField:leaf 分支引用按钮对所有叶子参数生效(含数组元素内字段), 与「表单展示」勾选同级;对象/数组字段本身不提供引用 - modelParamUtils:buildModelRequestParamsPath 恢复所有叶子保留 valueSource, 保存链路与编辑器语义一致 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -65,7 +65,9 @@
|
||||
</div>
|
||||
<div v-if="node.outputFields.length === 0" class="fcd-ref-node-empty">该节点无输出字段</div>
|
||||
<div v-for="of in node.outputFields" :key="of.field" class="fcd-ref-field" @click="setValueSource(node, of)">
|
||||
{{ of.label || of.field }}
|
||||
<span v-if="of.fieldType === 'object'" class="fcd-ref-type" title="引用对象整体">{} 对象</span>
|
||||
<span v-else-if="of.fieldType === 'array'" class="fcd-ref-type" title="引用数组整体">[] 数组</span>
|
||||
<span class="fcd-ref-field-text">{{ of.label || of.field }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -206,7 +208,9 @@
|
||||
</div>
|
||||
<div v-if="node.outputFields.length === 0" class="fcd-ref-node-empty">该节点无输出字段</div>
|
||||
<div v-for="of in node.outputFields" :key="of.field" class="fcd-ref-field" @click="setValueSource(node, of)">
|
||||
{{ of.label || of.field }}
|
||||
<span v-if="of.fieldType === 'object'" class="fcd-ref-type" title="引用对象整体">{} 对象</span>
|
||||
<span v-else-if="of.fieldType === 'array'" class="fcd-ref-type" title="引用数组整体">[] 数组</span>
|
||||
<span class="fcd-ref-field-text">{{ of.label || of.field }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -305,7 +309,7 @@ interface UpstreamNodeInfo {
|
||||
id: string;
|
||||
label: string;
|
||||
nodeCode: string;
|
||||
outputFields: { field: string; label: string }[];
|
||||
outputFields: { field: string; label: string; fieldType?: 'scalar' | 'object' | 'array' }[];
|
||||
}
|
||||
|
||||
interface FieldFormData {
|
||||
@@ -584,6 +588,9 @@ function handleClosed() { resetForm(); }
|
||||
padding: 2px 0 2px 8px;
|
||||
}
|
||||
.fcd-ref-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
padding: 4px 8px;
|
||||
@@ -593,6 +600,24 @@ function handleClosed() { resetForm(); }
|
||||
background: #eff6ff;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.fcd-ref-type {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
color: #64748b;
|
||||
background: #f1f5f9;
|
||||
border-radius: 3px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.fcd-ref-field-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<!-- 引用上级节点输出:按钮 + 候选面板(叶子字段 + 对象/数组整体) -->
|
||||
<el-popover
|
||||
v-if="canValueSource"
|
||||
placement="bottom"
|
||||
:width="260"
|
||||
trigger="click"
|
||||
popper-class="mf-ref-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
type="primary"
|
||||
class="mf-ref-btn"
|
||||
title="引用上级节点输出"
|
||||
:disabled="!!fieldDef?.runtimeShow || !hasUpstream"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="mf-ref-panel">
|
||||
<div v-if="!hasUpstream" class="mf-ref-empty">无可引用的上级节点</div>
|
||||
<template v-else>
|
||||
<div v-for="node in upstreamNodes" :key="node.id" class="mf-ref-node">
|
||||
<div class="mf-ref-node-head">
|
||||
<span class="mf-ref-node-name">{{ node.label }}</span>
|
||||
<el-tag size="small" type="info">{{ node.nodeCode }}</el-tag>
|
||||
</div>
|
||||
<div v-if="node.outputFields.length === 0" class="mf-ref-node-empty">该节点无输出字段</div>
|
||||
<div
|
||||
v-for="of in node.outputFields"
|
||||
:key="`${of.field}-${of.fieldType || 'scalar'}`"
|
||||
class="mf-ref-field"
|
||||
@click="setValueSource(node, of)"
|
||||
>
|
||||
<span v-if="of.fieldType === 'object'" class="mf-ref-type" title="引用对象整体">{} 对象</span>
|
||||
<span v-else-if="of.fieldType === 'array'" class="mf-ref-type" title="引用数组整体">[] 数组</span>
|
||||
<span class="mf-ref-field-text">{{ of.label || of.field }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { Link } from '@element-plus/icons-vue';
|
||||
|
||||
defineOptions({ name: 'MfReferenceButton' });
|
||||
|
||||
// 前驱节点的可引用输出字段(fieldType:scalar 叶子 / object、array 整体)
|
||||
interface UpstreamField {
|
||||
field: string;
|
||||
label: string;
|
||||
fieldType?: 'scalar' | 'object' | 'array';
|
||||
}
|
||||
interface UpstreamNodeInfo {
|
||||
id: string;
|
||||
label: string;
|
||||
nodeCode: string;
|
||||
outputFields: UpstreamField[];
|
||||
}
|
||||
|
||||
const props = defineProps<{ fieldDef: any; upstreamNodes?: UpstreamNodeInfo[] }>();
|
||||
|
||||
// 可引用:非开关、非固定选项下拉(开关/下拉值来自模型);叶子与对象/数组整体均可引用
|
||||
const canValueSource = computed(() => {
|
||||
const ft = props.fieldDef?.fieldType || 'string';
|
||||
const dt = props.fieldDef?.type || 'string';
|
||||
return ft !== 'switch' && ft !== 'select' && dt !== 'switch';
|
||||
});
|
||||
const hasUpstream = computed(() => (props.upstreamNodes?.length ?? 0) > 0);
|
||||
|
||||
// 选中前驱节点输出字段:就地写 fieldDef.valueSource,复用父组件深 watch 上传链路
|
||||
const setValueSource = (node: UpstreamNodeInfo, of: UpstreamField) => {
|
||||
if (!props.fieldDef) return;
|
||||
props.fieldDef.valueSource = { nodeId: node.id, field: of.field };
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mf-ref-btn {
|
||||
flex-shrink: 0;
|
||||
margin-left: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- popover teleport 到 body,面板样式需脱离 scoped -->
|
||||
<style lang="scss">
|
||||
.mf-ref-popover {
|
||||
.mf-ref-panel {
|
||||
// 面板超高时内部滚动,避免超出浏览器视口被截掉显示不全
|
||||
max-height: 40vh;
|
||||
overflow-y: auto;
|
||||
.mf-ref-empty {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
padding: 6px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mf-ref-node {
|
||||
& + .mf-ref-node {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.mf-ref-node-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.mf-ref-node-name {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.mf-ref-node-empty {
|
||||
font-size: 12px;
|
||||
color: #cbd5e1;
|
||||
padding: 2px 0 2px 8px;
|
||||
}
|
||||
|
||||
.mf-ref-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #eff6ff;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.mf-ref-type {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
color: #64748b;
|
||||
background: #f1f5f9;
|
||||
border-radius: 3px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.mf-ref-field-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -71,52 +71,14 @@
|
||||
class="mf-leaf-expose"
|
||||
>表单展示</el-checkbox>
|
||||
</el-tooltip>
|
||||
<el-popover
|
||||
v-if="canValueSource"
|
||||
placement="bottom"
|
||||
:width="260"
|
||||
trigger="click"
|
||||
popper-class="mf-ref-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
type="primary"
|
||||
class="mf-ref-btn"
|
||||
title="引用上级节点输出"
|
||||
:disabled="!!def.runtimeShow || !hasUpstream"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
<div class="mf-ref-panel">
|
||||
<div v-if="!hasUpstream" class="mf-ref-empty">无可引用的上级节点</div>
|
||||
<template v-else>
|
||||
<div v-for="node in upstreamNodes" :key="node.id" class="mf-ref-node">
|
||||
<div class="mf-ref-node-head">
|
||||
<span class="mf-ref-node-name">{{ node.label }}</span>
|
||||
<el-tag size="small" type="info">{{ node.nodeCode }}</el-tag>
|
||||
</div>
|
||||
<div v-if="node.outputFields.length === 0" class="mf-ref-node-empty">该节点无输出字段</div>
|
||||
<div
|
||||
v-for="of in node.outputFields"
|
||||
:key="of.field"
|
||||
class="mf-ref-field"
|
||||
@click="setValueSource(node, of)"
|
||||
>
|
||||
{{ of.label || of.field }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-popover>
|
||||
<!-- 引用上级输出与「表单展示」勾选同级:所有叶子参数(含数组元素内字段)都可引用 -->
|
||||
<MfReferenceButton :field-def="def" :upstream-nodes="upstreamNodes" />
|
||||
</div>
|
||||
<div v-if="hasValueSource" class="mf-ref-bound" :class="{ 'is-invalid': valueSourceInvalid }">
|
||||
<span class="mf-ref-bound-text" :title="valueSourceLabel">已引用:{{ valueSourceLabel }}</span>
|
||||
<el-button size="small" text type="danger" @click="clearValueSource">清除</el-button>
|
||||
</div>
|
||||
<div v-else-if="!isUpload" class="mf-leaf-ctrl">
|
||||
<div v-if="!hasValueSource && !isUpload" class="mf-leaf-ctrl">
|
||||
<el-input-number
|
||||
v-if="isNumber"
|
||||
:model-value="numberValue"
|
||||
@@ -160,7 +122,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue';
|
||||
import { FolderOpened, List, QuestionFilled, Link } from '@element-plus/icons-vue';
|
||||
import { FolderOpened, List, QuestionFilled } from '@element-plus/icons-vue';
|
||||
import MfReferenceButton from './MfReferenceButton.vue';
|
||||
|
||||
defineOptions({ name: 'ModelField' });
|
||||
|
||||
@@ -169,7 +132,7 @@ interface UpstreamNodeInfo {
|
||||
id: string;
|
||||
label: string;
|
||||
nodeCode: string;
|
||||
outputFields: { field: string; label: string }[];
|
||||
outputFields: { field: string; label: string; fieldType?: 'scalar' | 'object' | 'array' }[];
|
||||
}
|
||||
|
||||
const props = defineProps<{ fieldDef: any; path?: string; upstreamNodes?: UpstreamNodeInfo[] }>();
|
||||
@@ -203,9 +166,7 @@ const isTextarea = computed(() => isLeaf.value && fieldType.value === 'textarea'
|
||||
const isUpload = computed(() => isLeaf.value && fieldType.value === 'upload');
|
||||
|
||||
// ===== 引用上级节点输出 =====
|
||||
// 可引用:叶子 + 非开关 + 非固定选项下拉(开关/下拉值来自模型;上传类型同样支持引用上级输出)
|
||||
const canValueSource = computed(() => isLeaf.value && !isSwitch.value && !isSelect.value);
|
||||
const hasUpstream = computed(() => (props.upstreamNodes?.length ?? 0) > 0);
|
||||
// 引用按钮与候选面板在 MfReferenceButton 中统一渲染(叶子与对象/数组整体均可引用)
|
||||
const hasValueSource = computed(() => !!def.value?.valueSource);
|
||||
// 已引用时的展示文案:{节点label}.{字段label}
|
||||
const valueSourceLabel = computed(() => {
|
||||
@@ -237,12 +198,6 @@ watch(
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 选中前驱节点输出字段:就地写 def.valueSource,复用深 watch 上传链路
|
||||
const setValueSource = (node: UpstreamNodeInfo, of: { field: string; label: string }) => {
|
||||
if (!def.value) return;
|
||||
def.value.valueSource = { nodeId: node.id, field: of.field };
|
||||
};
|
||||
|
||||
const clearValueSource = () => {
|
||||
if (!def.value) return;
|
||||
delete def.value.valueSource;
|
||||
|
||||
@@ -288,6 +288,8 @@ function collectAllLeafFields(params: any, prefix = ''): { path: string; def: an
|
||||
}
|
||||
|
||||
// 生成 modelRequestParamsPath 扁平清单
|
||||
// 所有叶子字段(含对象/数组内部)都支持「引用上游」,valueSource 原样保留;
|
||||
// 与编辑器「引用上级输出与表单展示同级」语义一致
|
||||
export function buildModelRequestParamsPath(params: any, startNodeId = ''): ModelRequestParamsPathItem[] {
|
||||
const fields = collectAllLeafFields(params);
|
||||
return fields.map(({ path, def }) => {
|
||||
|
||||
@@ -149,6 +149,8 @@ interface RunFormField extends Omit<ExposedField, 'path'> {
|
||||
interface UpstreamField {
|
||||
field: string;
|
||||
label: string;
|
||||
// 输出字段类型:http 节点容器会额外产出 'object'/'array' 整体候选,标量或未标记视为叶子
|
||||
fieldType?: 'scalar' | 'object' | 'array';
|
||||
}
|
||||
interface UpstreamNodeInfo {
|
||||
id: string;
|
||||
@@ -729,50 +731,61 @@ const parseSchema = (value: any): any => {
|
||||
return null;
|
||||
};
|
||||
|
||||
// 递归收集 http 节点 response schema 的叶子输出字段(识别 JsonEditor 的 { type, attrs/value, label } 包裹结构):
|
||||
// 递归收集 http 节点 response schema 的输出字段(识别 JsonEditor 的 { type, attrs/value, label } 包裹结构):
|
||||
// 产出叶子字段 + 对象/数组容器整体候选(下游可引用整个结构);
|
||||
// field 用实际返回数据路径(不含 attrs 元数据,如 data.url 而非 data.attrs.url.value),
|
||||
// label 优先用字段配置的中文名(label),无则兜底路径 key
|
||||
const collectHttpLeafFields = (node: any, prefix = ''): UpstreamField[] => {
|
||||
const collectHttpOutputFields = (node: any, prefix = '', isRoot = true, suppressContainer = false): UpstreamField[] => {
|
||||
const out: UpstreamField[] = [];
|
||||
// JsonEditor 包裹结构:{ type: 'object'|'array'|'string'|..., attrs/value, label }
|
||||
if (node && typeof node === 'object' && !Array.isArray(node) && typeof node.type === 'string') {
|
||||
const jtype = node.type;
|
||||
if (['object', 'array', 'string', 'number', 'boolean', 'null'].includes(jtype)) {
|
||||
// 对象/数组容器:非根且非数组样本时,产出「整体」候选(引用整体 = 拿整个结构);标量不产整体
|
||||
if (!isRoot && !suppressContainer && prefix && (jtype === 'object' || jtype === 'array')) {
|
||||
out.push({ field: prefix, label: (typeof node.label === 'string' && node.label) || prefix, fieldType: jtype });
|
||||
}
|
||||
if (jtype === 'object') {
|
||||
// 对象容器:子字段在 attrs 里,field 路径直接拼子字段名,不拼 attrs
|
||||
const attrs = node.attrs;
|
||||
if (attrs && typeof attrs === 'object' && !Array.isArray(attrs)) {
|
||||
const out: UpstreamField[] = [];
|
||||
for (const k of Object.keys(attrs)) {
|
||||
out.push(...collectHttpLeafFields(attrs[k], prefix ? `${prefix}.${k}` : k));
|
||||
out.push(...collectHttpOutputFields(attrs[k], prefix ? `${prefix}.${k}` : k, false, false));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return [];
|
||||
return out;
|
||||
}
|
||||
if (jtype === 'array') {
|
||||
// 数组:取首元素样本作为结构(实际值为数组整体,field 不带索引)
|
||||
// 数组:取首元素样本作为结构(实际值为数组整体,field 不带索引);
|
||||
// 首元素为标量时不产叶子(数组整体已代表该元素),元素为对象时不产对象整体避免与数组整体 field 重复
|
||||
const arr = node.attrs;
|
||||
if (Array.isArray(arr) && arr.length > 0) return collectHttpLeafFields(arr[0], prefix);
|
||||
return [];
|
||||
if (Array.isArray(arr) && arr.length > 0) {
|
||||
const el = arr[0];
|
||||
if (el && typeof el === 'object' && !Array.isArray(el) && typeof el.type === 'string' && ['string', 'number', 'boolean', 'null'].includes(el.type)) {
|
||||
return out;
|
||||
}
|
||||
return out.concat(collectHttpOutputFields(el, prefix, false, true));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// 标量叶子:label 为字段配置的中文名,无则兜底路径
|
||||
return [{ field: prefix, label: (typeof node.label === 'string' && node.label) || prefix }];
|
||||
out.push({ field: prefix, label: (typeof node.label === 'string' && node.label) || prefix, fieldType: 'scalar' });
|
||||
return out;
|
||||
}
|
||||
}
|
||||
// 普通对象(根/未包裹):递归子字段;子字段为原始标量时以路径兜底产出
|
||||
if (node && typeof node === 'object' && !Array.isArray(node)) {
|
||||
const out: UpstreamField[] = [];
|
||||
for (const k of Object.keys(node)) {
|
||||
const child = node[k];
|
||||
const path = prefix ? `${prefix}.${k}` : k;
|
||||
if (child && typeof child === 'object') out.push(...collectHttpLeafFields(child, path));
|
||||
else out.push({ field: path, label: path });
|
||||
if (child && typeof child === 'object') out.push(...collectHttpOutputFields(child, path, false, false));
|
||||
else out.push({ field: path, label: path, fieldType: 'scalar' });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
// 数组根:取首元素结构样本
|
||||
if (Array.isArray(node)) return node.length > 0 ? collectHttpLeafFields(node[0], prefix) : [];
|
||||
return [];
|
||||
if (Array.isArray(node)) return node.length > 0 ? collectHttpOutputFields(node[0], prefix, isRoot, suppressContainer) : [];
|
||||
return out;
|
||||
};
|
||||
|
||||
// 取节点在设计时的可引用输出字段(运行时这些字段会产出实际值)
|
||||
@@ -786,7 +799,7 @@ const getNodeOutputFields = (node: Node<NodeData, any, string>): UpstreamField[]
|
||||
}));
|
||||
}
|
||||
if (nodeCode === 'http') {
|
||||
// http 节点:输出 = 结果返回结构(response schema)的所有叶子字段。
|
||||
// http 节点:输出 = 结果返回结构(response schema)的叶子字段 + 对象/数组容器整体。
|
||||
// 结果返回方式为主动拉取(responseType === 'pull')时,取主动拉取分支下配置的结果返回结构。
|
||||
const formConfig = node.data?.formConfig || [];
|
||||
const responseTypeEntry = formConfig.find((f: any) => f.field === 'responseType');
|
||||
@@ -795,7 +808,7 @@ const getNodeOutputFields = (node: Node<NodeData, any, string>): UpstreamField[]
|
||||
? (responseTypeEntry?.expand || []).find((e: any) => e.field === 'response')
|
||||
: formConfig.find((f: any) => f.field === 'response');
|
||||
const schema = parseSchema(responseField?.value);
|
||||
if (schema) return collectHttpLeafFields(schema);
|
||||
if (schema) return collectHttpOutputFields(schema);
|
||||
return [];
|
||||
}
|
||||
if (nodeCode === 'model') {
|
||||
|
||||
Reference in New Issue
Block a user