CMVR-IOT-UI/src/views/flow/nodes/function/switch.vue

719 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="node__container" ref="switchRef" @mouseleave="setNodeProperties">
<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'">
<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="连接多个下游分支,根据设定的条件按照顺序查找的方式来匹配运行的分支,如果匹配到某条件则只运行该条件对应的分支,否则继续匹配下一条件直至结束"
@setNodeName="setNodeName"
/>
<div class="condition_title_container">
<div class="left">
<div class="space"></div>
<div class="title">所有条件</div>
</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
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>
</div>
</div>
</template>
<script setup>
import { nextTick, onMounted, onUnmounted, reactive, ref, toRaw, 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({
model: Object,
graphModel: Object,
properties: Object,
nowTime: Number,
});
const emits = defineEmits([
"addAnchor",
"removeAnchor",
"bindRef",
"changeAnchor",
"syncAnchors",
]);
const switchRef = ref();
const dynamicForm = ref();
const flowStore = useFlowStore();
const rules = reactive({});
const { onFlowEvent } = useFlowEvents();
const formData = reactive({
nodeParams: props.properties?.conditions || [],
});
const addFormItem = () => {
addCondition(0);
};
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([]);
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;
}
}
};
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();
});
};
const addListItem = (pIndex, cIndex) => {
formData.nodeParams[pIndex].list.push({
name: "",
nameQuote: "",
nameType: "input",
type: "input",
input: "",
quote: "",
});
nextTick(() => {
scheduleAnchorSync();
});
};
const removeListItem = (pIndex, cIndex) => {
if (cIndex === 0) {
return;
}
formData.nodeParams[pIndex].list.splice(cIndex, 1);
nextTick(() => {
scheduleAnchorSync();
});
};
let anchorSyncFrame = null;
let resizeObserver = null;
const syncAnchorPositions = () => {
const root = switchRef.value;
if (!root) return;
const rootRect = root.getBoundingClientRect();
const scale = root.offsetWidth > 0 ? rootRect.width / root.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,
};
});
emits("syncAnchors", positions);
};
const scheduleAnchorSync = () => {
if (anchorSyncFrame !== null) cancelAnimationFrame(anchorSyncFrame);
anchorSyncFrame = requestAnimationFrame(() => {
anchorSyncFrame = null;
syncAnchorPositions();
});
};
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();
}
};
const setNodeName = (name) => {
const properties = lf.getProperties(props.model.id);
lf.setProperties(props.model.id, {
...properties,
name
});
emits("contentChange");
}
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);
const closePopover = () => {
if (nodeStateRef.value) {
nodeStateRef.value.closePopover();
}
}
const isEditing = ref(false)
const handleInputFocus = (e) => {
// 阻止事件冒泡到流程画布
e.stopPropagation()
e.preventDefault()
}
const handleInputKeydown = (e) => {
// 阻止事件冒泡到流程节点,避免被画布快捷键拦截
e.stopPropagation();
// 可选:明确放行 Ctrl+V增强兼容性
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
e.returnValue = true; // 允许默认粘贴行为
}
}
onMounted(() => {
emits("addAnchor", 0, "".concat(props.model.id, "_else"));
emits("bindRef", dynamicForm.value);
nextTick(() => {
scheduleAnchorSync();
resizeObserver = new ResizeObserver(scheduleAnchorSync);
if (switchRef.value) resizeObserver.observe(switchRef.value);
});
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");
}
});
onFlowEvent("contentChange", (data) => {
if (data.id === props.model.id) {
nodeOperatingStatus.value = "NORMAL";
inputJsonData.value = {};
outputJsonData.value = {};
emits("contentChange");
}
});
});
onUnmounted(() => {
resizeObserver?.disconnect();
if (anchorSyncFrame !== null) cancelAnimationFrame(anchorSyncFrame);
});
defineExpose({ closePopover })
</script>
<style lang="scss" scoped>
.node__container {
width: 100%;
height: auto;
background: #fff;
padding: 16px;
cursor: default;
border: 1px solid #dfe4ec;
border-left: 4px solid var(--node-accent, #b7791f);
border-radius: 8px;
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;
.condition_title_container {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid #e8ebf0;
.left {
display: flex;
align-items: center;
.space {
width: 3px;
height: 15px;
background: var(--node-accent, #b7791f);
border-radius: 3px;
}
.title {
margin-left: 8px;
color: #344054;
font-size: 13px;
font-weight: 650;
}
}
.el-button {
width: 28px;
height: 28px;
padding: 0;
border-color: #dfe4ec;
border-radius: 5px;
}
}
.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;
}
}
}
</style>