style: 调整分支节点样式
This commit is contained in:
parent
6c9ead2603
commit
92500c1c21
@ -8,7 +8,7 @@
|
||||
custom-class="flow-params-drawer"
|
||||
:title="props.data.properties?.name || '参数配置'"
|
||||
class="flow-params-drawer"
|
||||
size="min(560px, 92vw)"
|
||||
:size="drawerSize"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@ -36,6 +36,7 @@ import HttpNodeParams from './params/HttpNodeParams.vue'
|
||||
import SdAgentNodeParams from './params/SdAgentNodeParams.vue'
|
||||
import RecognizeNodeParams from './params/RecognizeNodeParams.vue'
|
||||
import DeviceUniversal from './params/device_universal.vue'
|
||||
import SwitchNodeParams from './params/SwitchNodeParams.vue'
|
||||
|
||||
const props = defineProps({
|
||||
drawer: Boolean,
|
||||
@ -45,6 +46,13 @@ const props = defineProps({
|
||||
const emits = defineEmits(['close'])
|
||||
const paramsComponentRef = ref(null)
|
||||
|
||||
/**
|
||||
* 分支条件包含左值、运算符和右值三列,仅为分支节点提供更宽的编辑空间。
|
||||
*/
|
||||
const drawerSize = computed(() => (
|
||||
props.data?.type === 'branch' ? 'min(920px, 96vw)' : 'min(560px, 92vw)'
|
||||
))
|
||||
|
||||
// 根据节点类型决定渲染哪个子组件
|
||||
const currentComponent = computed(() => {
|
||||
const type = props.data?.type
|
||||
@ -55,6 +63,7 @@ const currentComponent = computed(() => {
|
||||
if (type === 'sdAgent') return SdAgentNodeParams
|
||||
if (type === 'recognize') return RecognizeNodeParams
|
||||
if (type === 'device_universal') return DeviceUniversal
|
||||
if (type === 'branch') return SwitchNodeParams
|
||||
return null
|
||||
})
|
||||
|
||||
|
||||
413
src/views/flow/components/params/SwitchNodeParams.vue
Normal file
413
src/views/flow/components/params/SwitchNodeParams.vue
Normal file
@ -0,0 +1,413 @@
|
||||
<!--
|
||||
* 文件说明:分支节点专用参数组件,独立负责条件组的新增、删除、引用选择、校验和保存。
|
||||
* 作用范围:仅用于 branch 类型节点,不与通用节点参数表单共享业务逻辑。
|
||||
-->
|
||||
<template>
|
||||
<div class="switch-params">
|
||||
<div class="switch-params__header">
|
||||
<div>
|
||||
<div class="switch-params__title">分支条件</div>
|
||||
<div class="switch-params__description">条件按从上到下的顺序匹配,每个条件对应节点上的一个输出锚点。</div>
|
||||
</div>
|
||||
<el-button type="primary" :icon="Plus" @click="addCondition">添加条件</el-button>
|
||||
</div>
|
||||
|
||||
<el-form ref="formRef" :model="formData" label-position="top">
|
||||
<div v-for="(condition, conditionIndex) in formData.nodeParams" :key="condition.id" class="condition-card">
|
||||
<div class="condition-card__header">
|
||||
<div class="condition-card__identity">
|
||||
<span>{{ conditionIndex === 0 ? "If" : "Else If" }}</span>
|
||||
<el-input
|
||||
v-model="condition.label"
|
||||
maxlength="20"
|
||||
show-word-limit
|
||||
placeholder="请输入条件名称"
|
||||
class="condition-name"
|
||||
/>
|
||||
</div>
|
||||
<el-button
|
||||
circle
|
||||
plain
|
||||
type="danger"
|
||||
:icon="Minus"
|
||||
:disabled="formData.nodeParams.length === 1"
|
||||
@click="removeCondition(conditionIndex)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-form-item label="条件关系">
|
||||
<el-radio-group v-model="condition.withOr">
|
||||
<el-radio-button value="and">AND</el-radio-button>
|
||||
<el-radio-button value="or">OR</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<div v-for="(item, itemIndex) in condition.list" :key="itemIndex" class="condition-row">
|
||||
<div class="condition-row__fields">
|
||||
<el-form-item
|
||||
label="引用变量"
|
||||
:prop="`nodeParams.${conditionIndex}.list.${itemIndex}.name${item.nameType === 'quote' ? 'Quote' : ''}`"
|
||||
:rules="requiredRule(item.nameType === 'quote' ? '请选择引用变量' : '请输入变量值')"
|
||||
>
|
||||
<el-input v-if="item.nameType === 'input'" v-model="item.name" placeholder="请输入变量值">
|
||||
<template #prepend>
|
||||
<el-select v-model="item.nameType" class="value-type" @change="resetNameValue(item)">
|
||||
<el-option label="输入" value="input" />
|
||||
<el-option label="引用" value="quote" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-input>
|
||||
<div v-else class="reference-field">
|
||||
<el-select v-model="item.nameType" class="value-type" @change="resetNameValue(item)">
|
||||
<el-option label="输入" value="input" />
|
||||
<el-option label="引用" value="quote" />
|
||||
</el-select>
|
||||
<el-cascader
|
||||
v-model="item.nameQuote"
|
||||
:options="quoteOptions"
|
||||
:show-all-levels="false"
|
||||
placeholder="请选择"
|
||||
@visible-change="(visible) => refreshQuoteOptions(visible, item, 'nameQuote')"
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
label="判断方式"
|
||||
:prop="`nodeParams.${conditionIndex}.list.${itemIndex}.condition`"
|
||||
:rules="requiredRule('请选择判断方式')"
|
||||
>
|
||||
<el-select v-model="item.condition">
|
||||
<el-option v-for="option in conditionOptions" :key="option.value" :label="option.label" :value="option.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
label="比较值"
|
||||
:prop="`nodeParams.${conditionIndex}.list.${itemIndex}.${item.type === 'quote' ? 'quote' : 'input'}`"
|
||||
:rules="requiresComparison(item.condition) ? requiredRule(item.type === 'quote' ? '请选择比较变量' : '请输入比较值') : []"
|
||||
>
|
||||
<el-input
|
||||
v-if="item.type === 'input'"
|
||||
v-model="item.input"
|
||||
:disabled="!requiresComparison(item.condition)"
|
||||
placeholder="请输入比较值"
|
||||
>
|
||||
<template #prepend>
|
||||
<el-select v-model="item.type" class="value-type" @change="resetCompareValue(item)">
|
||||
<el-option label="输入" value="input" />
|
||||
<el-option label="引用" value="quote" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input v-else model-value="" readonly :disabled="!requiresComparison(item.condition)" placeholder="请选择比较变量">
|
||||
<template #prepend>
|
||||
<el-select v-model="item.type" class="value-type" @change="resetCompareValue(item)">
|
||||
<el-option label="输入" value="input" />
|
||||
<el-option label="引用" value="quote" />
|
||||
</el-select>
|
||||
</template>
|
||||
<template #append>
|
||||
<el-cascader
|
||||
v-model="item.quote"
|
||||
:disabled="!requiresComparison(item.condition)"
|
||||
:options="quoteOptions"
|
||||
:show-all-levels="false"
|
||||
@visible-change="(visible) => refreshQuoteOptions(visible, item, 'quote')"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="condition-row__actions">
|
||||
<el-button circle :icon="Plus" @click="addConditionItem(conditionIndex)" />
|
||||
<el-button
|
||||
circle
|
||||
:icon="Minus"
|
||||
:disabled="condition.list.length === 1"
|
||||
@click="removeConditionItem(conditionIndex, itemIndex)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<div class="else-tip"><strong>Else</strong><span>所有条件均不匹配时,从 Else 锚点继续执行。</span></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, watch } from "vue";
|
||||
import { Plus, Minus } from "@element-plus/icons-vue";
|
||||
import { v4 as randomUUID } from "uuid";
|
||||
import { getInput, removeSwitchEdge } from "@/utils/flow";
|
||||
|
||||
const props = defineProps({ data: Object });
|
||||
const emit = defineEmits(["save-success", "save-error"]);
|
||||
const formRef = ref(null);
|
||||
const quoteOptions = ref([]);
|
||||
const originalConditionIds = ref([]);
|
||||
const formData = reactive({ nodeParams: [] });
|
||||
|
||||
const conditionOptions = [
|
||||
{ label: "等于", value: "equal" },
|
||||
{ label: "不等于", value: "notEqualTo" },
|
||||
{ label: "为空", value: "null" },
|
||||
{ label: "不为空", value: "notNull" },
|
||||
{ label: "包含", value: "include" },
|
||||
{ label: "不包含", value: "notInclude" },
|
||||
{ label: "大于", value: "greaterThan" },
|
||||
{ label: "大于等于", value: "greaterThanOrEqual" },
|
||||
{ label: "小于", value: "lessThan" },
|
||||
{ label: "小于等于", value: "lessThanOrEqual" },
|
||||
];
|
||||
|
||||
/** 创建一条与原有数据结构兼容的判断项。 */
|
||||
const createConditionItem = () => ({
|
||||
name: "",
|
||||
nameQuote: "",
|
||||
nameType: "input",
|
||||
condition: "",
|
||||
type: "input",
|
||||
input: "",
|
||||
quote: "",
|
||||
});
|
||||
|
||||
/** 创建具有稳定锚点 ID 的条件组。 */
|
||||
const createCondition = (label = "") => ({
|
||||
id: randomUUID().replace(/-/g, ""),
|
||||
label,
|
||||
withOr: "and",
|
||||
list: [createConditionItem()],
|
||||
});
|
||||
|
||||
/**
|
||||
* 将旧版本可能保存的点号字符串统一转换为级联组件需要的路径数组。
|
||||
* @param {string|string[]} value 已保存的引用值
|
||||
* @returns {string[]} 级联选择路径
|
||||
*/
|
||||
const normalizeQuotePath = (value) => {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (typeof value === "string" && value) return value.split(".").filter(Boolean);
|
||||
return [];
|
||||
};
|
||||
|
||||
/** 深拷贝已保存条件,确保取消编辑时不会污染节点属性。 */
|
||||
const initData = () => {
|
||||
// 必须先提供选项树再赋已有值,否则级联组件无法将节点 ID 路径解析成可见标签。
|
||||
quoteOptions.value = getInput(props.data.id) || [];
|
||||
const source = props.data?.properties?.nodeParams || props.data?.properties?.conditions || [];
|
||||
formData.nodeParams = JSON.parse(JSON.stringify(source));
|
||||
if (!formData.nodeParams.length) formData.nodeParams.push(createCondition());
|
||||
formData.nodeParams.forEach((condition, index) => {
|
||||
if (!condition.label) condition.label = `条件 ${index + 1}`;
|
||||
condition.list?.forEach((item) => {
|
||||
if (item.nameType === "quote") item.nameQuote = normalizeQuotePath(item.nameQuote);
|
||||
if (item.type === "quote") item.quote = normalizeQuotePath(item.quote);
|
||||
});
|
||||
});
|
||||
originalConditionIds.value = formData.nodeParams.map((condition) => condition.id);
|
||||
};
|
||||
|
||||
/** 返回 Element Plus 表单必填规则。 */
|
||||
const requiredRule = (message) => [{ required: true, message, trigger: ["blur", "change"] }];
|
||||
|
||||
/** 判断当前运算符是否需要比较值。 */
|
||||
const requiresComparison = (condition) => !["null", "notNull"].includes(condition);
|
||||
|
||||
/** 新增一个分支条件,保存后将生成同 ID 的节点锚点。 */
|
||||
const addCondition = () => {
|
||||
formData.nodeParams.push(createCondition(`条件 ${formData.nodeParams.length + 1}`));
|
||||
};
|
||||
|
||||
/** 删除条件;实际连线清理延迟到保存成功时执行。 */
|
||||
const removeCondition = (index) => {
|
||||
if (formData.nodeParams.length > 1) formData.nodeParams.splice(index, 1);
|
||||
};
|
||||
|
||||
/** 向指定条件组追加判断项。 */
|
||||
const addConditionItem = (conditionIndex) => {
|
||||
formData.nodeParams[conditionIndex].list.push(createConditionItem());
|
||||
};
|
||||
|
||||
/** 删除指定判断项,同时保证每个条件至少保留一项。 */
|
||||
const removeConditionItem = (conditionIndex, itemIndex) => {
|
||||
const list = formData.nodeParams[conditionIndex].list;
|
||||
if (list.length > 1) list.splice(itemIndex, 1);
|
||||
};
|
||||
|
||||
/** 切换左值类型时清空另一种类型遗留的数据。 */
|
||||
const resetNameValue = (item) => {
|
||||
if (item.nameType === "input") item.nameQuote = "";
|
||||
else item.name = "";
|
||||
};
|
||||
|
||||
/** 切换比较值类型时清空另一种类型遗留的数据。 */
|
||||
const resetCompareValue = (item) => {
|
||||
if (item.type === "input") item.quote = "";
|
||||
else item.input = "";
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开引用选择器时获取当前节点可引用的上游输出,并保留当前选值。
|
||||
* @param {boolean} visible 选择器是否打开
|
||||
* @param {object} item 当前判断项
|
||||
* @param {"nameQuote"|"quote"} field 当前引用字段
|
||||
*/
|
||||
const refreshQuoteOptions = (visible, item, field) => {
|
||||
if (!visible) return;
|
||||
quoteOptions.value = getInput(props.data.id) || [];
|
||||
item[field] = item[field] || "";
|
||||
};
|
||||
|
||||
/**
|
||||
* 校验并保存条件。先清理已删除条件的连线,再写入条件和锚点集合。
|
||||
* 新条件锚点的位置由 switch.vue 渲染摘要后自动测量并补齐。
|
||||
*/
|
||||
const validateAndSave = async () => {
|
||||
const valid = await formRef.value?.validate().then(() => true).catch(() => false);
|
||||
if (!valid) return;
|
||||
|
||||
try {
|
||||
const conditions = JSON.parse(JSON.stringify(formData.nodeParams));
|
||||
const activeIds = new Set(conditions.map((condition) => condition.id));
|
||||
const removedIds = originalConditionIds.value.filter((id) => !activeIds.has(id));
|
||||
removedIds.forEach((id) => removeSwitchEdge(props.data.id, id));
|
||||
|
||||
const properties = window.lf.getProperties(props.data.id);
|
||||
const anchor = (properties.anchor || []).filter((item) => (
|
||||
item.id === `${props.data.id}_else` || activeIds.has(item.id)
|
||||
));
|
||||
window.lf.setProperties(props.data.id, {
|
||||
...properties,
|
||||
nodeParams: conditions,
|
||||
anchor,
|
||||
});
|
||||
emit("save-success");
|
||||
} catch {
|
||||
emit("save-error");
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => props.data, initData, { immediate: true, deep: true });
|
||||
defineExpose({ validateAndSave });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.switch-params {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.switch-params__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.switch-params__title {
|
||||
color: #1f2937;
|
||||
font-size: 16px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.switch-params__description {
|
||||
margin-top: 4px;
|
||||
color: #7a8494;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.condition-card {
|
||||
margin-bottom: 14px;
|
||||
padding: 14px;
|
||||
border: 1px solid #e4e8ef;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.condition-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
color: #3978c5;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.condition-card__identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.condition-name {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.condition-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed #dfe4ec;
|
||||
}
|
||||
|
||||
.condition-row + .condition-row {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.condition-row__fields {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.4fr) minmax(120px, 0.8fr) minmax(0, 1.4fr);
|
||||
flex: 1;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.condition-row__actions {
|
||||
display: flex;
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.value-type {
|
||||
width: 74px;
|
||||
}
|
||||
|
||||
.reference-field {
|
||||
display: grid;
|
||||
grid-template-columns: 74px minmax(0, 1fr);
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.else-tip {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #f0d9a7;
|
||||
border-radius: 8px;
|
||||
background: #fffaf0;
|
||||
color: #7a8494;
|
||||
font-size: 12px;
|
||||
|
||||
strong {
|
||||
color: #b7791f;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append) {
|
||||
width: 120px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append .el-cascader) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@ -354,7 +354,7 @@ const drop = (e) => {
|
||||
}
|
||||
|
||||
if (
|
||||
!["selectArea", "branch", "stopLoop", "subStart", "subEnd"].includes(newNode.type)
|
||||
!["selectArea", "stopLoop", "subStart", "subEnd"].includes(newNode.type)
|
||||
) {
|
||||
showParamsDrawer.value = true;
|
||||
paramsDrawerData.value = newNode;
|
||||
@ -383,7 +383,7 @@ const handleConnect = (connection) => {
|
||||
*/
|
||||
const handleNodeDoubleClick = ({ node }) => {
|
||||
const data = lf.getGraphData().nodes.find((item) => item.id === node.id);
|
||||
if (!data || ["selectArea", "branch", "stopLoop", "subStart", "subEnd", "end"].includes(data.type)) return;
|
||||
if (!data || ["selectArea", "stopLoop", "subStart", "subEnd", "end"].includes(data.type)) return;
|
||||
showParamsDrawer.value = true;
|
||||
paramsDrawerData.value = data;
|
||||
};
|
||||
@ -649,8 +649,8 @@ const initFlowData = {
|
||||
properties: {
|
||||
name: "Start",
|
||||
action: "start",
|
||||
nodeParams: [
|
||||
{ name: "robotId", type: "input", input: "", disabled: true }
|
||||
inputParams: [
|
||||
{ name: "robotId", type: "string", input: "", disabled: true }
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,281 +1,63 @@
|
||||
<!--
|
||||
* 文件说明:条件分支节点,配置条件组和动态分支锚点。
|
||||
* 作用范围:仅服务于流程设计器模块。
|
||||
* 文件说明:条件分支节点,展示已保存的条件摘要并同步每个条件对应的画布锚点。
|
||||
* 作用范围:仅服务于流程设计器模块;条件编辑由 SwitchNodeParams 组件独立负责。
|
||||
-->
|
||||
<template>
|
||||
<div class="node__container" ref="switchRef" @mouseleave="setNodeProperties">
|
||||
<div class="node__container" ref="switchRef">
|
||||
<NodeState :state="nodeOperatingStatus" :runtimes="runtimes" ref="nodeStateRef">
|
||||
<template #input>
|
||||
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
|
||||
</template>
|
||||
<template #output>
|
||||
<div v-if="nodeOperatingStatus != 'FAILED'">
|
||||
<div v-if="nodeOperatingStatus !== 'FAILED'">
|
||||
<JsonViewer :value="outputJsonData" copyable boxed sort theme="light" />
|
||||
</div>
|
||||
<div v-else>{{ errorInfoData }}</div>
|
||||
</template>
|
||||
</NodeState>
|
||||
|
||||
<NodeTitle
|
||||
:icon="switchSvg"
|
||||
:nodeId="props.model.id"
|
||||
:nodeProperties="props.properties"
|
||||
:nodeName="props.properties?.name || '分支'"
|
||||
nodeDesc="连接多个下游分支,根据设定的条件按照顺序查找的方式来匹配运行的分支,如果匹配到某条件则只运行该条件对应的分支,否则继续匹配下一条件直至结束"
|
||||
nodeDesc="双击节点配置分支条件;流程将按顺序匹配条件,并只运行首个匹配条件对应的分支"
|
||||
@setNodeName="setNodeName"
|
||||
/>
|
||||
<div class="condition_title_container">
|
||||
<div class="left">
|
||||
<div class="space"></div>
|
||||
<div class="title">所有条件</div>
|
||||
|
||||
<div class="condition-summary">
|
||||
<div class="condition-summary__title">
|
||||
<span>分支条件</span>
|
||||
<span class="condition-summary__count">{{ conditions.length }} 个</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<el-button
|
||||
:disabled="flowStore.disableForm"
|
||||
@click="addFormItem"
|
||||
class="addFormItem"
|
||||
>
|
||||
<el-icon :size="20"><Plus /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-form
|
||||
:inline="true"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
ref="dynamicForm"
|
||||
label-position="top"
|
||||
label-width="auto"
|
||||
:disabled="flowStore.disableForm"
|
||||
|
||||
<div
|
||||
v-for="(condition, index) in conditions"
|
||||
:id="condition.id"
|
||||
:key="condition.id"
|
||||
:data-branch-anchor-id="condition.id"
|
||||
class="condition-summary__item"
|
||||
>
|
||||
<div
|
||||
class="condition"
|
||||
v-for="(params, index) in formData.nodeParams"
|
||||
:key="params.id"
|
||||
:id="params.id"
|
||||
:data-branch-anchor-id="params.id"
|
||||
>
|
||||
<div class="title__container">
|
||||
<div class="left">
|
||||
<div class="space"></div>
|
||||
<div class="title">{{ index === 0 ? "If" : "Else If" }}</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<el-button
|
||||
:disabled="flowStore.disableForm"
|
||||
circle
|
||||
size="small"
|
||||
@click="deleteFormItem(params.id, index)"
|
||||
>
|
||||
<el-icon :size="16"><Minus /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="withOr">
|
||||
<div class="title">条件</div>
|
||||
<div>
|
||||
<el-select
|
||||
v-model="params.withOr"
|
||||
placeholder="Select"
|
||||
style="width: 240px"
|
||||
@focus="handleInputFocus"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
>
|
||||
<el-option key="and" label="AND" value="and" />
|
||||
<el-option key="or" label="OR" value="or" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form__container">
|
||||
<el-row v-for="(property, pIndex) in params.list" :key="`${params.id}-${pIndex}`">
|
||||
<el-form-item
|
||||
:label="pIndex === 0 ? '引用变量' : ''"
|
||||
:prop="`nodeParams.${index}.list.${pIndex}.nameType`"
|
||||
>
|
||||
<el-select
|
||||
v-model="property.nameType"
|
||||
@focus="handleInputFocus"
|
||||
@keydown="handleInputKeydown"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
@change="handleTypeChange(index, pIndex)"
|
||||
>
|
||||
<el-option label="引用" value="quote" />
|
||||
<el-option label="输入" value="input" />
|
||||
</el-select>
|
||||
<el-form-item
|
||||
v-if="property.nameType === 'input'"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入参数值',
|
||||
trigger: 'blur',
|
||||
},
|
||||
]"
|
||||
:prop="`nodeParams.${index}.list.${pIndex}.name`"
|
||||
>
|
||||
<el-input
|
||||
v-model="property.name"
|
||||
placeholder="请输入"
|
||||
clearable
|
||||
@focus="handleInputFocus"
|
||||
@keydown="handleInputKeydown"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="property.nameType === 'quote'"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择参数值',
|
||||
trigger: 'blur',
|
||||
},
|
||||
]"
|
||||
:prop="`nodeParams.${index}.list.${pIndex}.nameQuote`"
|
||||
>
|
||||
<el-cascader
|
||||
v-model="property.nameQuote"
|
||||
:options="quoteOptions"
|
||||
placeholder="请选择"
|
||||
:key="nameQuoteKey"
|
||||
@visible-change="(val) => visibleChange(val, 'nameQuote', index, pIndex)"
|
||||
@focus="handleInputFocus"
|
||||
@keydown="handleInputKeydown"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="pIndex === 0 ? '选择条件' : ''"
|
||||
:prop="`nodeParams.${index}.list.${pIndex}.condition`"
|
||||
:rules="[
|
||||
{ required: true, message: '请输入变量名', trigger: 'blur' },
|
||||
]"
|
||||
>
|
||||
<el-select
|
||||
v-model="property.condition"
|
||||
@focus="handleInputFocus"
|
||||
@keydown="handleInputKeydown"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
>
|
||||
<el-option label="等于" value="equal" />
|
||||
<el-option label="不等于" value="notEqualTo" />
|
||||
<el-option label="为空" value="null" />
|
||||
<el-option label="不为空" value="notNull" />
|
||||
<el-option label="包含" value="include" />
|
||||
<el-option label="不包含" value="notInclude" />
|
||||
<el-option label="大于" value="greaterThan" />
|
||||
<el-option label="大于等于" value="greaterThanOrEqual" />
|
||||
<el-option label="小于" value="lessThan" />
|
||||
<el-option label="小于等于" value="lessThanOrEqual" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="pIndex === 0 ? '比较值' : ''"
|
||||
:prop="`nodeParams.${index}.list.${pIndex}.type`"
|
||||
>
|
||||
<el-select
|
||||
v-model="property.type"
|
||||
@change="handleTypeChange(index, pIndex)"
|
||||
@focus="handleInputFocus"
|
||||
@keydown="handleInputKeydown"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
>
|
||||
<el-option label="引用" value="quote" />
|
||||
<el-option label="输入" value="input" />
|
||||
</el-select>
|
||||
<el-form-item
|
||||
v-if="property.type === 'input'"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入参数值',
|
||||
trigger: 'blur',
|
||||
},
|
||||
]"
|
||||
:prop="`nodeParams.${index}.list.${pIndex}.input`"
|
||||
>
|
||||
<el-input
|
||||
v-model="property.input"
|
||||
placeholder="请输入"
|
||||
clearable
|
||||
@focus="handleInputFocus"
|
||||
@keydown="handleInputKeydown"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="property.type === 'quote'"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请选择参数值',
|
||||
trigger: 'blur',
|
||||
},
|
||||
]"
|
||||
:prop="`nodeParams.${index}.list.${pIndex}.quote`"
|
||||
>
|
||||
<el-cascader
|
||||
v-model="property.quote"
|
||||
:options="quoteOptions"
|
||||
placeholder="请选择"
|
||||
@visible-change="(val) => visibleChange(val, 'quote', index, pIndex)"
|
||||
@focus="handleInputFocus"
|
||||
@keydown="handleInputKeydown"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form-item>
|
||||
<el-form-item :label="pIndex === 0 ? '操作' : ''">
|
||||
<el-button
|
||||
:disabled="flowStore.disableForm"
|
||||
circle
|
||||
size="small"
|
||||
@click="addListItem(index)"
|
||||
>
|
||||
<el-icon :size="12"><Plus /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
:disabled="flowStore.disableForm"
|
||||
circle
|
||||
size="small"
|
||||
@click="removeListItem(index, pIndex)"
|
||||
>
|
||||
<el-icon :size="12"><Minus /></el-icon>
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
<div
|
||||
class="else"
|
||||
:id="''.concat(props.model.id, '_else')"
|
||||
:data-branch-anchor-id="''.concat(props.model.id, '_else')"
|
||||
>
|
||||
<div class="space"></div>
|
||||
<div class="title">Else</div>
|
||||
<span class="condition-summary__order">{{ condition.label || `条件 ${index + 1}` }}</span>
|
||||
<span class="condition-summary__text">{{ conditionSummary(condition) }}</span>
|
||||
</div>
|
||||
<div
|
||||
:id="`${props.model.id}_else`"
|
||||
:data-branch-anchor-id="`${props.model.id}_else`"
|
||||
class="condition-summary__item condition-summary__item--else"
|
||||
>
|
||||
<span class="condition-summary__order">Else</span>
|
||||
<span class="condition-summary__text">以上条件均不满足时执行</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, onUnmounted, reactive, ref, toRaw, watch } from "vue";
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import NodeTitle from "../../components/NodeTitle.vue";
|
||||
import NodeState from "../../components/NodeState.vue";
|
||||
import switchSvg from "../../icon/switch.svg";
|
||||
import { v4 as randomUUID } from "uuid";
|
||||
import { useFlowStore } from "@/store/modules/flow";
|
||||
import { Plus, Minus } from "@element-plus/icons-vue";
|
||||
import { getInput, removeSwitchEdge } from "@/utils/flow";
|
||||
import { useFlowEvents } from "../../composables/useFlowEvents";
|
||||
|
||||
const props = defineProps({
|
||||
@ -285,152 +67,100 @@ const props = defineProps({
|
||||
nowTime: Number,
|
||||
});
|
||||
|
||||
const emits = defineEmits([
|
||||
"addAnchor",
|
||||
"removeAnchor",
|
||||
"bindRef",
|
||||
"changeAnchor",
|
||||
"syncAnchors",
|
||||
]);
|
||||
|
||||
const switchRef = ref();
|
||||
const dynamicForm = ref();
|
||||
const flowStore = useFlowStore();
|
||||
const rules = reactive({});
|
||||
const emits = defineEmits(["contentChange", "syncAnchors"]);
|
||||
const { onFlowEvent } = useFlowEvents();
|
||||
const switchRef = ref(null);
|
||||
const nodeStateRef = ref(null);
|
||||
const conditions = computed(() => props.properties?.nodeParams || props.properties?.conditions || []);
|
||||
|
||||
const formData = reactive({
|
||||
nodeParams: props.properties?.conditions || [],
|
||||
});
|
||||
|
||||
/**
|
||||
* 新增 addFormItem 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
*/
|
||||
const addFormItem = () => {
|
||||
addCondition(0);
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除 deleteFormItem 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} id 目标元素 ID
|
||||
* @param {*} index 目标项索引
|
||||
*/
|
||||
const deleteFormItem = (id, index) => {
|
||||
if (index === 0) {
|
||||
return;
|
||||
}
|
||||
formData.nodeParams.splice(index, 1);
|
||||
emits("removeAnchor", id);
|
||||
removeSwitchEdge(props.model.id, id);
|
||||
nextTick(() => {
|
||||
scheduleAnchorSync();
|
||||
});
|
||||
};
|
||||
|
||||
const quoteOptions = ref([]);
|
||||
/**
|
||||
* 处理 handleTypeChange 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} pIndex 条件组索引
|
||||
* @param {*} index 目标项索引
|
||||
*/
|
||||
const handleTypeChange = (pIndex, index) => {
|
||||
if (formData.nodeParams[pIndex].list[index].type === "input") {
|
||||
formData.nodeParams[pIndex].list[index].quote = "";
|
||||
} else {
|
||||
formData.nodeParams[pIndex].list[index].input = "";
|
||||
const option = getInput(props.model.id, false);
|
||||
if (option) {
|
||||
quoteOptions.value = option;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 新增 addCondition 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} height 锚点纵向位置
|
||||
*/
|
||||
const addCondition = (height) => {
|
||||
const id = randomUUID().replace(/-/g, "");
|
||||
formData.nodeParams.push({
|
||||
id,
|
||||
withOr: "and",
|
||||
list: [
|
||||
{
|
||||
name: "",
|
||||
nameQuote: "",
|
||||
nameType: "input",
|
||||
type: "input",
|
||||
input: "",
|
||||
quote: "",
|
||||
},
|
||||
],
|
||||
});
|
||||
emits("addAnchor", height, id);
|
||||
|
||||
nextTick(() => {
|
||||
scheduleAnchorSync();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 新增 addListItem 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} pIndex 条件组索引
|
||||
* @param {*} cIndex 条件项索引
|
||||
*/
|
||||
const addListItem = (pIndex, cIndex) => {
|
||||
formData.nodeParams[pIndex].list.push({
|
||||
name: "",
|
||||
nameQuote: "",
|
||||
nameType: "input",
|
||||
type: "input",
|
||||
input: "",
|
||||
quote: "",
|
||||
});
|
||||
nextTick(() => {
|
||||
scheduleAnchorSync();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 移除 removeListItem 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} pIndex 条件组索引
|
||||
* @param {*} cIndex 条件项索引
|
||||
*/
|
||||
const removeListItem = (pIndex, cIndex) => {
|
||||
if (cIndex === 0) {
|
||||
return;
|
||||
}
|
||||
formData.nodeParams[pIndex].list.splice(cIndex, 1);
|
||||
nextTick(() => {
|
||||
scheduleAnchorSync();
|
||||
});
|
||||
};
|
||||
|
||||
const nodeOperatingStatus = ref("NORMAL");
|
||||
const runtimes = ref(0);
|
||||
const inputJsonData = ref({});
|
||||
const outputJsonData = ref({});
|
||||
const errorInfoData = ref("");
|
||||
let anchorSyncFrame = null;
|
||||
let resizeObserver = null;
|
||||
|
||||
const conditionTextMap = {
|
||||
equal: "等于",
|
||||
notEqualTo: "不等于",
|
||||
null: "为空",
|
||||
notNull: "不为空",
|
||||
include: "包含",
|
||||
notInclude: "不包含",
|
||||
greaterThan: "大于",
|
||||
greaterThanOrEqual: "大于等于",
|
||||
lessThan: "小于",
|
||||
lessThanOrEqual: "小于等于",
|
||||
};
|
||||
|
||||
/**
|
||||
* 同步 syncAnchorPositions 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* 生成画布卡片中的完整条件说明,多个判断项按配置的 AND/OR 关系连接。
|
||||
* @param {object} condition 当前条件组
|
||||
* @returns {string} 可直接展示的条件表达式
|
||||
*/
|
||||
const conditionSummary = (condition) => {
|
||||
if (!condition.list?.length) return "未配置判断";
|
||||
const relation = condition.withOr === "or" ? " OR " : " AND ";
|
||||
return condition.list.map((item) => {
|
||||
const left = item.nameType === "quote"
|
||||
? formatQuote(item.nameQuote)
|
||||
: String(item.name || "未填写变量");
|
||||
const operator = conditionTextMap[item.condition] || "未选择判断";
|
||||
if (["null", "notNull"].includes(item.condition)) return `${left} ${operator}`;
|
||||
const right = item.type === "quote"
|
||||
? formatQuote(item.quote)
|
||||
: String(item.input ?? "未填写比较值");
|
||||
return `${left} ${operator} ${right}`;
|
||||
}).join(relation);
|
||||
};
|
||||
|
||||
/**
|
||||
* 将级联引用路径格式化为易读文本。
|
||||
* @param {string|string[]} quote 引用路径
|
||||
* @returns {string} 点号连接的引用名称
|
||||
*/
|
||||
const formatQuote = (quote) => {
|
||||
const path = Array.isArray(quote)
|
||||
? quote.filter(Boolean)
|
||||
: String(quote || "").split(".").filter(Boolean);
|
||||
if (!path.length) return "未选择引用";
|
||||
|
||||
const [nodeId, direction, ...fieldPath] = path;
|
||||
const graphNode = window.lf?.getGraphData().nodes.find((node) => node.id === nodeId);
|
||||
const nodeName = graphNode?.properties?.name || graphNode?.name || nodeId;
|
||||
const directionName = direction === "input"
|
||||
? "输入"
|
||||
: direction === "output" ? "输出" : direction;
|
||||
return [nodeName, directionName, ...fieldPath].filter(Boolean).join(".");
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据摘要项的实际纵向中心同步分支锚点;范围仅限当前分支节点。
|
||||
*/
|
||||
const syncAnchorPositions = () => {
|
||||
// 从实际渲染后的条件卡片读取纵向中心点,再批量通知节点壳更新锚点。
|
||||
// 批量同步可避免条件增删时逐个更新导致边路径在同一帧内反复抖动。
|
||||
const root = switchRef.value;
|
||||
if (!root) return;
|
||||
const rootRect = root.getBoundingClientRect();
|
||||
const scale = root.offsetWidth > 0 ? rootRect.width / root.offsetWidth : 1;
|
||||
// Handle 的 top 相对于 FlowNode 外壳定位,不能使用 switch 根元素作为坐标原点。
|
||||
// 外层卡片存在 padding;若以 rootRect.top 计算,所有锚点都会统一向上偏移。
|
||||
const positioningParent = root.closest(".flow-node-shell");
|
||||
if (!positioningParent) return;
|
||||
const parentRect = positioningParent.getBoundingClientRect();
|
||||
const scale = positioningParent.offsetWidth > 0
|
||||
? parentRect.width / positioningParent.offsetWidth
|
||||
: 1;
|
||||
const positions = [...root.querySelectorAll("[data-branch-anchor-id]")].map((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
id: element.dataset.branchAnchorId,
|
||||
height: Math.round(((rect.top - rootRect.top + rect.height / 2) / scale) * 100) / 100,
|
||||
height: Math.round(((rect.top - parentRect.top + rect.height / 2) / scale) * 100) / 100,
|
||||
};
|
||||
});
|
||||
emits("syncAnchors", positions);
|
||||
};
|
||||
|
||||
/**
|
||||
* 调度 scheduleAnchorSync 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* 将同一帧内的多次尺寸变化合并为一次锚点同步;范围仅限当前分支节点。
|
||||
*/
|
||||
const scheduleAnchorSync = () => {
|
||||
if (anchorSyncFrame !== null) cancelAnimationFrame(anchorSyncFrame);
|
||||
@ -441,124 +171,29 @@ const scheduleAnchorSync = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置 setNodeProperties 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
*/
|
||||
const setNodeProperties = async () => {
|
||||
try {
|
||||
const valid = await dynamicForm.value.validate();
|
||||
if (valid) {
|
||||
const data = toRaw(formData);
|
||||
const properties = lf.getProperties(props.model.id);
|
||||
lf.setProperties(props.model.id, {
|
||||
...properties,
|
||||
...data,
|
||||
});
|
||||
emits("contentChange");
|
||||
}
|
||||
} catch (error) {
|
||||
dynamicForm.value.clearValidate();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置 setNodeName 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} name 名称或事件名称
|
||||
* 保存节点名称;范围仅限当前分支节点。
|
||||
* @param {string} name 新的节点名称
|
||||
*/
|
||||
const setNodeName = (name) => {
|
||||
const properties = lf.getProperties(props.model.id);
|
||||
lf.setProperties(props.model.id, {
|
||||
...properties,
|
||||
name
|
||||
});
|
||||
const properties = window.lf.getProperties(props.model.id);
|
||||
window.lf.setProperties(props.model.id, { ...properties, name });
|
||||
emits("contentChange");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 visibleChange 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} value 待处理的新值
|
||||
* @param {*} type 目标类型
|
||||
* @param {*} index 目标项索引
|
||||
* @param {*} pIndex 条件组索引
|
||||
*/
|
||||
const visibleChange = async (value, type, index, pIndex) => {
|
||||
if (value) {
|
||||
const option = getInput(props.model.id);
|
||||
if (option) {
|
||||
quoteOptions.value = option;
|
||||
const currentValue = formData.nodeParams[index].list[pIndex][type]
|
||||
if (currentValue) {
|
||||
formData.nodeParams[index].list[pIndex][type] = null
|
||||
await nextTick();
|
||||
formData.nodeParams[index].list[pIndex][type] = currentValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const nodeOperatingStatus = ref("NORMAL");
|
||||
const runtimes = ref(0);
|
||||
const inputJsonData = ref({});
|
||||
const outputJsonData = ref({});
|
||||
const errorInfoData = ref('');
|
||||
|
||||
watch(
|
||||
() => props.properties,
|
||||
() => {
|
||||
if (props.properties.nodeParams && props.properties.nodeParams.length > 0) {
|
||||
formData.nodeParams = props.properties.nodeParams;
|
||||
} else {
|
||||
if (formData.nodeParams.length === 0) {
|
||||
addCondition(100);
|
||||
}
|
||||
}
|
||||
nextTick(scheduleAnchorSync);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
const nodeStateRef = ref(null);
|
||||
|
||||
/**
|
||||
* 关闭 closePopover 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* 关闭节点运行状态浮层;范围仅限当前分支节点。
|
||||
*/
|
||||
const closePopover = () => {
|
||||
if (nodeStateRef.value) {
|
||||
nodeStateRef.value.closePopover();
|
||||
}
|
||||
}
|
||||
nodeStateRef.value?.closePopover();
|
||||
};
|
||||
|
||||
const isEditing = ref(false)
|
||||
/**
|
||||
* 处理 handleInputFocus 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} e 触发操作的浏览器事件
|
||||
*/
|
||||
const handleInputFocus = (e) => {
|
||||
// 阻止事件冒泡到流程画布
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理 handleInputKeydown 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} e 触发操作的浏览器事件
|
||||
*/
|
||||
const handleInputKeydown = (e) => {
|
||||
// 阻止事件冒泡到流程节点,避免被画布快捷键拦截
|
||||
e.stopPropagation();
|
||||
// 可选:明确放行 Ctrl+V(增强兼容性)
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
|
||||
e.returnValue = true; // 允许默认粘贴行为
|
||||
}
|
||||
}
|
||||
watch(
|
||||
() => conditions.value.map((condition) => condition.id),
|
||||
() => nextTick(scheduleAnchorSync),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
emits("addAnchor", 0, "".concat(props.model.id, "_else"));
|
||||
|
||||
emits("bindRef", dynamicForm.value);
|
||||
nextTick(() => {
|
||||
scheduleAnchorSync();
|
||||
resizeObserver = new ResizeObserver(scheduleAnchorSync);
|
||||
@ -566,36 +201,26 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
onFlowEvent("changeNodeState", (data) => {
|
||||
if (data.nodeId === props.model.id) {
|
||||
nodeOperatingStatus.value = data.status;
|
||||
if (data.endTime && data.startTime) {
|
||||
runtimes.value = data.endTime - data.startTime;
|
||||
}
|
||||
let inputData = {};
|
||||
let output = {};
|
||||
errorInfoData.value = data?.message || ''
|
||||
try {
|
||||
inputData = JSON.parse(data.paramsIn) || {};
|
||||
output = JSON.parse(data.paramsOut) || {};
|
||||
} catch (error) {
|
||||
inputData = data.paramsIn || {};
|
||||
output = data.paramsOut || {};
|
||||
}
|
||||
inputJsonData.value = inputData;
|
||||
outputJsonData.value = output;
|
||||
|
||||
emits("contentChange");
|
||||
if (data.nodeId !== props.model.id) return;
|
||||
nodeOperatingStatus.value = data.status;
|
||||
if (data.endTime && data.startTime) runtimes.value = data.endTime - data.startTime;
|
||||
errorInfoData.value = data?.message || "";
|
||||
try {
|
||||
inputJsonData.value = JSON.parse(data.paramsIn) || {};
|
||||
outputJsonData.value = JSON.parse(data.paramsOut) || {};
|
||||
} catch {
|
||||
inputJsonData.value = data.paramsIn || {};
|
||||
outputJsonData.value = data.paramsOut || {};
|
||||
}
|
||||
emits("contentChange");
|
||||
});
|
||||
|
||||
onFlowEvent("contentChange", (data) => {
|
||||
if (data.id === props.model.id) {
|
||||
nodeOperatingStatus.value = "NORMAL";
|
||||
inputJsonData.value = {};
|
||||
outputJsonData.value = {};
|
||||
|
||||
emits("contentChange");
|
||||
}
|
||||
if (data.id !== props.model.id) return;
|
||||
nodeOperatingStatus.value = "NORMAL";
|
||||
inputJsonData.value = {};
|
||||
outputJsonData.value = {};
|
||||
emits("contentChange");
|
||||
});
|
||||
});
|
||||
|
||||
@ -604,179 +229,91 @@ onUnmounted(() => {
|
||||
if (anchorSyncFrame !== null) cancelAnimationFrame(anchorSyncFrame);
|
||||
});
|
||||
|
||||
defineExpose({ closePopover })
|
||||
defineExpose({ closePopover });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.node__container {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background: #fff;
|
||||
min-height: 180px;
|
||||
padding: 16px;
|
||||
cursor: default;
|
||||
border: 1px solid #dfe4ec;
|
||||
border-left: 4px solid var(--node-accent, #b7791f);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 8px 24px rgba(31, 41, 55, 0.09);
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
transition: border-color 0.16s ease, box-shadow 0.16s ease;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.condition_title_container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 14px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e8ebf0;
|
||||
.condition-summary {
|
||||
margin-top: 14px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e8ebf0;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.condition-summary__title,
|
||||
.condition-summary__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.space {
|
||||
width: 3px;
|
||||
height: 15px;
|
||||
background: var(--node-accent, #b7791f);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.condition-summary__title {
|
||||
margin-bottom: 8px;
|
||||
color: #344054;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-left: 8px;
|
||||
color: #344054;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
}
|
||||
}
|
||||
.condition-summary__count {
|
||||
color: #98a2b3;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border-color: #dfe4ec;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
.condition-summary__item {
|
||||
min-height: 44px;
|
||||
margin-top: 6px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #e4e8ef;
|
||||
border-radius: 6px;
|
||||
background: #f8fafc;
|
||||
position: relative;
|
||||
|
||||
.condition {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin: 8px 0;
|
||||
padding: 11px 12px 5px;
|
||||
border: 1px solid #e4e8ef;
|
||||
border-radius: 7px;
|
||||
background-color: #f8fafc;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
width: 17px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -18px;
|
||||
border-top: 1px solid var(--node-accent, #b7791f);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.title__container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.space {
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
background: #3978c5;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-left: 8px;
|
||||
color: #344054;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.withOr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 9px 0 7px;
|
||||
|
||||
.title {
|
||||
color: #7a8494;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-select) { width: 120px !important; }
|
||||
}
|
||||
|
||||
:deep(.form__container) {
|
||||
.el-form-item {
|
||||
margin-right: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
width: 105px;
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 105px;
|
||||
}
|
||||
|
||||
.el-cascader {
|
||||
width: 105px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.else {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 40px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e4e8ef;
|
||||
border-radius: 7px;
|
||||
background-color: #f8fafc;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
width: 17px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -18px;
|
||||
border-top: 1px solid var(--node-accent, #b7791f);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.space {
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
background: #697386;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-left: 8px;
|
||||
color: #344054;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
&::after {
|
||||
content: "";
|
||||
width: 17px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -18px;
|
||||
border-top: 1px solid var(--node-accent, #b7791f);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.condition-summary__item--else {
|
||||
background: #fffaf0;
|
||||
}
|
||||
|
||||
.condition-summary__order {
|
||||
width: 96px;
|
||||
flex: 0 0 96px;
|
||||
color: #3978c5;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.condition-summary__text {
|
||||
flex: 1;
|
||||
color: #7a8494;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
overflow-wrap: anywhere;
|
||||
text-align: right;
|
||||
white-space: normal;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
:is-valid-connection="isValidLoopConnection"
|
||||
class="flow-handle flow-handle--target"
|
||||
/>
|
||||
<div class="flow-node-content" :class="{ 'flow-node-card': !isGroup && flowType !== 'branch' }">
|
||||
<div class="flow-node-content" :class="{ 'flow-node-card': !isGroup }">
|
||||
<component
|
||||
:is="nodeComponent"
|
||||
ref="componentRef"
|
||||
@ -132,6 +132,7 @@ const branchAnchors = computed(() => {
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
const model = computed(() => window.lf?.getNodeModelById(props.id));
|
||||
|
||||
/**
|
||||
@ -375,4 +376,5 @@ onMounted(() => bindComponent(componentRef.value));
|
||||
&--branch:hover { transform: translate(50%, -50%) scale(1.18); }
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@ -50,12 +50,13 @@ const groupChildExtent = () => ({
|
||||
* @param {*} node 当前流程节点
|
||||
*/
|
||||
const nodeSize = (node) => {
|
||||
// 分支节点采用统一的标准卡片尺寸,忽略旧流程数据中遗留的大尺寸记录。
|
||||
if (node.type === "branch") return { width: 372, height: 180 };
|
||||
const width = Number(node.properties?.width || node.width);
|
||||
const height = Number(node.properties?.height || node.height);
|
||||
if (Number.isFinite(width) && Number.isFinite(height)) {
|
||||
return { width, height };
|
||||
}
|
||||
if (node.type === "branch") return { width: 748, height: 420 };
|
||||
if (GROUP_TYPES.has(node.type)) return { width: 720, height: 460 };
|
||||
if (TARGET_ONLY_TYPES.has(node.type) || SOURCE_ONLY_TYPES.has(node.type)) {
|
||||
return { width: 328, height: 120 };
|
||||
|
||||
Loading…
Reference in New Issue
Block a user