feat: 清除上一次运行结果

This commit is contained in:
zhanghao 2026-04-15 10:59:17 +08:00
parent 8c1820e1a6
commit 783e54bab0
21 changed files with 1393 additions and 1371 deletions

View File

@ -1,22 +1,22 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
export const useFlowStore = defineStore('flow', { export const useFlowStore = defineStore('flow', {
state: () => ({ state: () => ({
disableForm: false, disableForm: false,
deviceList: [] deviceList: []
}), }),
actions: { actions: {
updateDisableForm(value) { updateDisableForm(value) {
this.disableForm = value this.disableForm = value
}, },
async getDeviceList() { async getDeviceList() {
const data = await new Promise((resolve, reject) => { const data = await new Promise((resolve, reject) => {
resolve( ['cam1', 'cam2', 'cam3', 'cam4']) resolve( ['cam1', 'cam2', 'cam3', 'cam4'])
}) })
this.deviceList = data this.deviceList = data
return data return data
} }
} }
} }
) )

View File

@ -1,51 +1,57 @@
<template> <template>
<div> <div>
<div class="state__container" :class="`nodeState-${props.state}`"> <div class="state__container" :class="`nodeState-${props.state}`">
<div class="context"> <div class="context" v-show="showState !== 'NORMAL'">
<div class="state"> <div class="state">
<el-icon> <el-icon>
<SuccessFilled v-if="props.state === 'SUCCESS'" /> <SuccessFilled v-if="props.state === 'SUCCESS'" />
<CircleCloseFilled v-if="['FAILED', 'STOPPED'].includes(props.state)" /> <CircleCloseFilled v-if="['FAILED', 'STOPPED'].includes(props.state)" />
<VideoPause v-if="props.state === 'PAUSED'" /> <VideoPause v-if="props.state === 'PAUSED'" />
<Loading v-if="props.state === 'RUNNING'" /> <Loading v-if="props.state === 'RUNNING'" />
</el-icon> </el-icon>
<div class="text">{{ getStateText() }}</div> <div class="text">{{ getStateText() }}</div>
<el-tag :type="props.state === 'SUCCESS' ? 'success' : 'danger'"> <el-tag :type="props.state === 'SUCCESS' ? 'success' : 'danger'">
{{ props.runtimes }}ms {{ props.runtimes }}ms
</el-tag> </el-tag>
</div>
<div class="resultText" v-popover="popoverRef" @click="popoverVisible = !popoverVisible">{{ popoverVisible ? '隐藏结果' : '展示结果' }}</div>
</div> </div>
<div
class="resultText"
v-popover="popoverRef"
@click="popoverVisible = !popoverVisible"
>
{{ popoverVisible ? "隐藏结果" : "展示结果" }}
</div> </div>
</div>
<el-popover
ref="popoverRef"
virtual-triggering
persistent
placement="right-start"
:visible="popoverVisible"
:teleported="false"
popper-class="popover__container"
>
<template #default>
<div class="params__container input">
<div class="paramsTitle">输入</div>
<div>
<slot name="input"></slot>
</div>
</div>
<div class="params__container output" v-if="slots.output">
<div class="paramsTitle">输出</div>
<div>
<slot name="output"></slot>
</div>
</div>
</template>
</el-popover>
</div> </div>
<el-popover
ref="popoverRef"
virtual-triggering
persistent
placement="right-start"
:visible="popoverVisible"
:teleported="false"
popper-class="popover__container"
>
<template #default>
<div class="params__container input">
<div class="paramsTitle">输入</div>
<div>
<slot name="input"></slot>
</div>
</div>
<div class="params__container output" v-if="slots.output">
<div class="paramsTitle">输出</div>
<div>
<slot name="output"></slot>
</div>
</div>
</template>
</el-popover>
</div>
</template> </template>
<script setup lang="js"> <script setup lang="js">
import { ref, useSlots } from 'vue' import { ref, useSlots, watch } from 'vue'
import { SuccessFilled, CircleCloseFilled, Loading, VideoPause } from '@element-plus/icons-vue' import { SuccessFilled, CircleCloseFilled, Loading, VideoPause } from '@element-plus/icons-vue'
const props = defineProps({ const props = defineProps({
@ -56,7 +62,7 @@ const props = defineProps({
runtimes: { runtimes: {
type: Number, type: Number,
default: 0 default: 0
} },
}) })
const slots = useSlots() const slots = useSlots()
@ -76,83 +82,95 @@ const getStateText = () => {
const popoverRef = ref(); const popoverRef = ref();
const popoverVisible = ref(false); const popoverVisible = ref(false);
const showState = ref('NORMAL')
watch(() => props.state, (newVal) => {
showState.value = newVal;
});
const closePopover = () => {
showState.value = 'NORMAL';
popoverVisible.value = false;
};
defineExpose({ closePopover })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.state__container { .state__container {
display: none; display: none;
margin-bottom: 8px; margin-bottom: 8px;
.context { .context {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px;
font-size: 12px;
.state {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
padding: 4px;
font-size: 12px;
.state { .text {
display: flex; margin: 0 8px;
align-items: center;
.text {
margin: 0 8px;
}
}
.resultText {
color: #1664ff;
cursor: pointer;
} }
} }
}
.nodeState-SUCCESS { .resultText {
display: block; color: #1664ff;
background-color: #eef9f1; cursor: pointer;
.el-icon {
font-size: 16px;
color: #309256;
} }
} }
}
.nodeState-FAILED { .nodeState-SUCCESS {
display: block; display: block;
background-color: #fdf5f5; background-color: #eef9f1;
.el-icon { .el-icon {
font-size: 16px; font-size: 16px;
color: #ee3f38; color: #309256;
}
} }
}
.nodeState-STOPPED { .nodeState-FAILED {
display: block; display: block;
background-color: #fdf5f5; background-color: #fdf5f5;
.el-icon { .el-icon {
font-size: 16px; font-size: 16px;
color: #ee3f38; color: #ee3f38;
}
} }
}
.nodeState-PAUSED { .nodeState-STOPPED {
display: block; display: block;
background-color: #cbcbcb; background-color: #fdf5f5;
.el-icon { .el-icon {
font-size: 16px; font-size: 16px;
color: #666; color: #ee3f38;
}
} }
}
.nodeState-RUNNING { .nodeState-PAUSED {
display: block; display: block;
background-color: #f4f7ff; background-color: #cbcbcb;
.el-icon { .el-icon {
color: #68a2e4; font-size: 16px;
font-size: 16px; color: #666;
animation: rotateAnimation 2s linear infinite;
}
} }
</style> }
.nodeState-RUNNING {
display: block;
background-color: #f4f7ff;
.el-icon {
color: #68a2e4;
font-size: 16px;
animation: rotateAnimation 2s linear infinite;
}
}
</style>

View File

@ -8,21 +8,12 @@
</div> </div>
<div class="btn__container"> <div class="btn__container">
<div class="btn"> <div class="btn">
<el-button v-if="['testRunError', 'testRunFinish', 'stop'].includes(flowState)" @click="clearRun">清除上一次运行结果</el-button>
<el-button @click="group">分组</el-button> <el-button @click="group">分组</el-button>
<el-button v-if="flowState !== 'testRunning'" @click="testRun" <el-button v-if="flowState !== 'testRunning'" @click="testRun">试运行</el-button>
>试运行</el-button <el-button v-if="flowState === 'testRunning'" @click="flowPauseFn">暂停</el-button>
> <el-button v-if="flowState === 'pause'" @click="flowResumeFn">恢复</el-button>
<el-button v-if="flowState === 'testRunning'" @click="flowPauseFn" <el-button v-if="['testRunning', 'pause'].includes(flowState)" @click="flowStopFn">终止</el-button>
>暂停</el-button
>
<el-button v-if="flowState === 'pause'" @click="flowResumeFn"
>恢复</el-button
>
<el-button
v-if="['testRunning', 'pause'].includes(flowState)"
@click="flowStopFn"
>终止</el-button
>
</div> </div>
<div class="btn"> <div class="btn">
<el-tooltip <el-tooltip
@ -34,9 +25,7 @@
> >
<el-button type="primary" :disabled="true">发布</el-button> <el-button type="primary" :disabled="true">发布</el-button>
</el-tooltip> </el-tooltip>
<el-button v-else @click="deployFlow" type="primary" <el-button v-else @click="deployFlow" type="primary">发布</el-button>
>发布</el-button
>
<el-popover <el-popover
:width="260" :width="260"
@ -123,9 +112,7 @@
<el-form-item <el-form-item
:label="index === 0 ? '参数名' : ''" :label="index === 0 ? '参数名' : ''"
:prop="`nodeParams.${index}.name`" :prop="`nodeParams.${index}.name`"
:rules="[ :rules="[{ required: true, message: '请输入参数名', trigger: 'blur' }]"
{ required: true, message: '请输入参数名', trigger: 'blur' },
]"
> >
<el-input <el-input
:disabled="property?.disabled || false" :disabled="property?.disabled || false"
@ -137,9 +124,7 @@
<el-form-item <el-form-item
:label="index === 0 ? '参数值' : ''" :label="index === 0 ? '参数值' : ''"
:prop="`nodeParams.${index}.input`" :prop="`nodeParams.${index}.input`"
:rules="[ :rules="[{ required: true, message: '请输入参数值', trigger: 'blur' }]"
{ required: true, message: '请输入参数值', trigger: 'blur' },
]"
> >
<el-input-number <el-input-number
v-if="property.componentType === 'number'" v-if="property.componentType === 'number'"
@ -176,9 +161,7 @@
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="visible = false">取消</el-button> <el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="execute"> <el-button type="primary" @click="execute"> 确定 </el-button>
确定
</el-button>
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
@ -203,7 +186,7 @@ import {
flowPause, flowPause,
flowResume, flowResume,
flowStop, flowStop,
flowAction flowAction,
} from "@/api/device/flow"; } from "@/api/device/flow";
import { getDetect } from "@/api/test/detect"; import { getDetect } from "@/api/test/detect";
import { Location } from "@element-plus/icons-vue"; import { Location } from "@element-plus/icons-vue";
@ -233,9 +216,11 @@ const drop = (e) => {
}, },
}); });
if (!["selectArea", 'branch', 'stopLoop', 'subStart', 'subEnd'].includes(newNode.type)) { if (
!["selectArea", "branch", "stopLoop", "subStart", "subEnd"].includes(newNode.type)
) {
showParamsDrawer.value = true; showParamsDrawer.value = true;
console.log('newNode', newNode) console.log("newNode", newNode);
paramsDrawerData.value = newNode; paramsDrawerData.value = newNode;
} }
}; };
@ -444,8 +429,7 @@ const loopFlowView = async (instId, taskId = null) => {
} else { } else {
if (!flowInfoData.value.isLog) { if (!flowInfoData.value.isLog) {
if (["PAUSED", "STOPPED"].includes(arr[arr.length - 1]?.status)) { if (["PAUSED", "STOPPED"].includes(arr[arr.length - 1]?.status)) {
flowState.value = flowState.value = arr[arr.length - 1]?.status === "PAUSED" ? "pause" : "stop";
arr[arr.length - 1]?.status === "PAUSED" ? "pause" : "stop";
} else { } else {
flowState.value = "testRunError"; flowState.value = "testRunError";
flowStore.updateDisableForm(false); flowStore.updateDisableForm(false);
@ -562,10 +546,7 @@ const initFlow = () => {
keys: ["backspace", "delete"], // keys: ["backspace", "delete"], //
callback: () => { callback: () => {
const activeElem = document.activeElement; const activeElem = document.activeElement;
if ( if (activeElem.tagName === "INPUT" || activeElem.tagName === "TEXTAREA") {
activeElem.tagName === "INPUT" ||
activeElem.tagName === "TEXTAREA"
) {
return; // return; //
} }
@ -657,10 +638,12 @@ const initFlow = () => {
}); });
lf.on("node:dbclick", ({ data, e }) => { lf.on("node:dbclick", ({ data, e }) => {
if (["selectArea", 'branch', 'stopLoop', 'subStart', 'subEnd'].includes(data.type)) { if (
["selectArea", "branch", "stopLoop", "subStart", "subEnd"].includes(data.type)
) {
return; return;
} }
console.log('data', data) console.log("data", data);
showParamsDrawer.value = true; showParamsDrawer.value = true;
paramsDrawerData.value = data; paramsDrawerData.value = data;
}); });
@ -734,55 +717,65 @@ const fitView = () => {
}; };
const nodeName = ref(""); const nodeName = ref("");
const visible = ref(false) const visible = ref(false);
const formData = reactive({ const formData = reactive({
nodeParams: [], nodeParams: [],
}) });
const rules = reactive({}) const rules = reactive({});
const executeForm = ref() const executeForm = ref();
const nodeAction = ref('') const nodeAction = ref("");
const singleNodeExecution = (e) => { const singleNodeExecution = (e) => {
const { name, nodeType, nodeParams, action } = e.detail; const { name, nodeType, nodeParams, action } = e.detail;
nodeName.value = name; nodeName.value = name;
visible.value = true; visible.value = true;
nodeAction.value = action nodeAction.value = action;
if (nodeType === 'EDGE') { if (nodeType === "EDGE") {
formData.nodeParams = [ formData.nodeParams = [
{ {
disabled: true, disabled: true,
input: "", input: "",
name: "terminalId", name: "terminalId",
type: "input" type: "input",
}, },
...nodeParams ...nodeParams,
] ];
} else { } else {
formData.nodeParams = nodeParams formData.nodeParams = nodeParams;
} }
}; };
const execute = async () => { const execute = async () => {
const result = await executeForm.value.validate(); const result = await executeForm.value.validate();
if (result) { if (result) {
const obj = {} const obj = {};
formData.nodeParams.forEach(item => { formData.nodeParams.forEach((item) => {
obj[item.name] = item.input obj[item.name] = item.input;
}) });
const res = await flowAction({ const res = await flowAction({
action: nodeAction.value, action: nodeAction.value,
payload: obj payload: obj,
}) });
if (res.code === 200) { if (res.code === 200) {
ElMessage.success('执行成功') ElMessage.success("执行成功");
} else { } else {
ElMessage.error(res.msg) ElMessage.error(res.msg);
} }
} }
} };
const showParamsDrawer = ref(false); const showParamsDrawer = ref(false);
const paramsDrawerData = ref({}) const paramsDrawerData = ref({});
const clearRun = () => {
const { nodes } = lf.getGraphData();
nodes.forEach((item) => {
const node = lf.getNodeModelById(item.id);
if (node.closePopover) {
node.closePopover();
}
});
}
onMounted(() => { onMounted(() => {
justLoadFlow(); justLoadFlow();
@ -876,8 +869,6 @@ onUnmounted(() => {
} }
} }
} }
</style> </style>
<style lang="scss"> <style lang="scss">
.node-relationship-network-popover { .node-relationship-network-popover {
@ -903,16 +894,16 @@ onUnmounted(() => {
} }
.el-dialog { .el-dialog {
.el-input { .el-input {
width: 200px; width: 200px;
} }
.el-select { .el-select {
width: 200px; width: 200px;
} }
.el-textarea { .el-textarea {
width: 200px width: 200px;
} }
} }
</style> </style>

View File

@ -144,16 +144,10 @@ class ServiceNodeHtmlModel extends HtmlNodeModel {
this.componentInstance = instance; this.componentInstance = instance;
} }
// 验证表单 closePopover() {
async validateForm() { this.componentInstance.closePopover()
const result = this.componentInstance.validateForm()
return result
} }
// setCustomProperties() {
// this.componentInstance.setNodeProperties()
// }
/** /**
* 设置节点属性 * 设置节点属性
* 包括宽度高度文本编辑属性等 * 包括宽度高度文本编辑属性等

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="node__container" :class="props.model.id"> <div class="node__container" :class="props.model.id">
<NodeState :state="nodeOperatingStatus" :runtimes="runtimes"> <NodeState :state="nodeOperatingStatus" :runtimes="runtimes" ref="nodeStateRef">
<template #input> <template #input>
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" /> <JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
</template> </template>
@ -78,6 +78,14 @@ const inputJsonData = ref({});
const outputJsonData = ref({}); const outputJsonData = ref({});
const errorInfoData = ref(''); const errorInfoData = ref('');
const nodeStateRef = ref(null);
const closePopover = () => {
if (nodeStateRef.value) {
nodeStateRef.value.closePopover();
}
}
onMounted(() => { onMounted(() => {
emitter.on("changeNodeState", (data) => { emitter.on("changeNodeState", (data) => {
@ -124,6 +132,8 @@ onUnmounted(() => {
emitter.off("setProperties"); emitter.off("setProperties");
}); });
defineExpose({ closePopover })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -142,6 +142,10 @@ class StartNodeHtmlModel extends HtmlNodeModel {
this.componentInstance = instance; this.componentInstance = instance;
} }
closePopover() {
this.componentInstance.closePopover()
}
/** /**
* 设置节点属性 * 设置节点属性
* 包括宽度高度文本编辑属性等 * 包括宽度高度文本编辑属性等

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="node__container" :class="props.model.id"> <div class="node__container" :class="props.model.id">
<keep-alive> <keep-alive>
<NodeState :state="nodeOperatingStatus" :runtimes="runtimes"> <NodeState :state="nodeOperatingStatus" :runtimes="runtimes" ref="nodeStateRef">
<template #input> <template #input>
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" /> <JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
</template> </template>
@ -40,6 +40,14 @@ const runtimes = ref(0);
const inputJsonData = ref({}); const inputJsonData = ref({});
const outputJsonData = ref({}); const outputJsonData = ref({});
const nodeStateRef = ref(null);
const closePopover = () => {
if (nodeStateRef.value) {
nodeStateRef.value.closePopover();
}
}
onMounted(() => { onMounted(() => {
emitter.on("changeNodeState", (data) => { emitter.on("changeNodeState", (data) => {
if (data.nodeId === props.model.id) { if (data.nodeId === props.model.id) {
@ -87,6 +95,8 @@ onUnmounted(() => {
emitter.off("setProperties"); emitter.off("setProperties");
}); });
defineExpose({ closePopover })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -144,6 +144,10 @@ class CodeNodeHtmlModel extends HtmlNodeModel {
this.componentInstance = instance; this.componentInstance = instance;
} }
closePopover() {
this.componentInstance.closePopover()
}
/** /**
* 设置节点属性 * 设置节点属性
* 包括宽度高度文本编辑属性等 * 包括宽度高度文本编辑属性等

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="node__container" :class="props.model.id"> <div class="node__container" :class="props.model.id">
<NodeState :state="nodeOperatingStatus" :runtimes="runtimes"> <NodeState :state="nodeOperatingStatus" :runtimes="runtimes" ref="nodeStateRef">
<template #input> <template #input>
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" /> <JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
</template> </template>
@ -74,6 +74,15 @@ const inputJsonData = ref({});
const outputJsonData = ref({}); const outputJsonData = ref({});
const errorInfoData = ref(''); const errorInfoData = ref('');
const nodeStateRef = ref(null);
const closePopover = () => {
if (nodeStateRef.value) {
nodeStateRef.value.closePopover();
}
}
onMounted(() => { onMounted(() => {
emitter.on("changeNodeState", (data) => { emitter.on("changeNodeState", (data) => {
if (data.nodeId === props.model.id) { if (data.nodeId === props.model.id) {
@ -119,6 +128,7 @@ onUnmounted(() => {
emitter.off("setProperties"); emitter.off("setProperties");
}); });
defineExpose({ closePopover })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="node__container" :class="props.model.id"> <div class="node__container" :class="props.model.id">
<NodeState :state="nodeOperatingStatus" :runtimes="runtimes"> <NodeState :state="nodeOperatingStatus" :runtimes="runtimes" ref="nodeStateRef">
<template #input> <template #input>
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" /> <JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
</template> </template>
@ -46,12 +46,6 @@ const props = defineProps({
const emits = defineEmits(["contentChange"]); const emits = defineEmits(["contentChange"]);
const formData = reactive({
nodeParams: [],
});
const setNodeName = (name) => { const setNodeName = (name) => {
const properties = lf.getProperties(props.model.id); const properties = lf.getProperties(props.model.id);
lf.setProperties(props.model.id, { lf.setProperties(props.model.id, {
@ -66,6 +60,14 @@ const runtimes = ref(0);
const inputJsonData = ref({}); const inputJsonData = ref({});
const outputJsonData = ref({}); const outputJsonData = ref({});
const nodeStateRef = ref(null);
const closePopover = () => {
if (nodeStateRef.value) {
nodeStateRef.value.closePopover();
}
}
onMounted(() => { onMounted(() => {
emitter.on("changeNodeState", (data) => { emitter.on("changeNodeState", (data) => {
if (data.nodeId === props.model.id) { if (data.nodeId === props.model.id) {
@ -96,13 +98,22 @@ onMounted(() => {
emits("contentChange"); emits("contentChange");
} }
}); });
emitter.on("setProperties", (data) => {
if (data.id === props.model.id) {
emits("contentChange");
}
});
}); });
onUnmounted(() => { onUnmounted(() => {
emitter.off("changeNodeState"); emitter.off("changeNodeState");
emitter.off("contentChange"); emitter.off("contentChange");
emitter.off("setProperties");
}); });
defineExpose({ closePopover })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -150,9 +150,9 @@ class CurrentLoopNodeHtmlModel extends HtmlNodeModel {
return result return result
} }
// setCustomProperties() { closePopover() {
// this.componentInstance.setNodeProperties() this.componentInstance.closePopover()
// } }
/** /**
* 设置节点属性 * 设置节点属性

View File

@ -144,15 +144,9 @@ class HttpNodeHtmlModel extends HtmlNodeModel {
this.componentInstance = instance; this.componentInstance = instance;
} }
// 验证表单 closePopover() {
// async validateForm() { this.componentInstance.closePopover()
// const result = this.componentInstance.validateForm() }
// return result
// }
// setCustomProperties() {
// this.componentInstance.setNodeProperties()
// }
/** /**
* 设置节点属性 * 设置节点属性

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="node__container" :class="props.model.id"> <div class="node__container" :class="props.model.id">
<NodeState :state="nodeOperatingStatus" :runtimes="runtimes"> <NodeState :state="nodeOperatingStatus" :runtimes="runtimes" ref="nodeStateRef">
<template #input> <template #input>
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" /> <JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
</template> </template>
@ -77,6 +77,13 @@ const inputJsonData = ref({});
const outputJsonData = ref({}); const outputJsonData = ref({});
const errorInfoData = ref(''); const errorInfoData = ref('');
const nodeStateRef = ref(null);
const closePopover = () => {
if (nodeStateRef.value) {
nodeStateRef.value.closePopover();
}
}
onMounted(() => { onMounted(() => {
emitter.on("changeNodeState", (data) => { emitter.on("changeNodeState", (data) => {
@ -122,6 +129,8 @@ onUnmounted(() => {
emitter.off("contentChange"); emitter.off("contentChange");
emitter.off("setProperties"); emitter.off("setProperties");
}); });
defineExpose({ closePopover })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,219 +1,213 @@
// 导入 HtmlNode 及其模型,为后续继承做准备 // 导入 HtmlNode 及其模型,为后续继承做准备
import { HtmlNode, HtmlNodeModel } from "@logicflow/core"; import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
// 导入 Vue 相关方法,用于渲染组件 // 导入 Vue 相关方法,用于渲染组件
import { createApp, h, nextTick } from "vue"; import { createApp, h, nextTick } from "vue";
import ElementPlus from "element-plus"; import ElementPlus from "element-plus";
// 导入 Vue 组件 // 导入 Vue 组件
// @ts-ignore // @ts-ignore
import Sleep from "./sleep.vue"; import Sleep from "./sleep.vue";
import OuterNode from "../../components/OuterNode.vue"; import OuterNode from "../../components/OuterNode.vue";
import JsonViewer from 'vue3-json-viewer' import JsonViewer from 'vue3-json-viewer'
/** /**
* 定义一个 元素的 HTML 节点类继承自 HtmlNode * 定义一个 元素的 HTML 节点类继承自 HtmlNode
* 该类负责在 HTML 中渲染 元素并处理其交互逻辑 * 该类负责在 HTML 中渲染 元素并处理其交互逻辑
*/ */
class SleepNodeHtmlNode extends HtmlNode { class SleepNodeHtmlNode extends HtmlNode {
resizeObserver = null; resizeObserver = null;
isMounted; // 标记组件是否已挂载 isMounted; // 标记组件是否已挂载
r; // 渲染函数 r; // 渲染函数
app; // Vue 应用实例 app; // Vue 应用实例
container = null; container = null;
static reusePool = new Map(); // 节点复用池 static reusePool = new Map(); // 节点复用池
/** /**
* 构造函数 * 构造函数
* @param props 传递给节点的属性包括模型图模型等 * @param props 传递给节点的属性包括模型图模型等
*/ */
constructor(props) { constructor(props) {
super(props); super(props);
this.initVueApp(props); this.initVueApp(props);
} }
initVueApp(props) { initVueApp(props) {
this.isMounted = false; this.isMounted = false;
this.hideAnchor = false; this.hideAnchor = false;
this.autoExpand = true; // 防止锚点被折叠 this.autoExpand = true; // 防止锚点被折叠
this.anchorsPreset = "default"; // 重置锚点预设 this.anchorsPreset = "default"; // 重置锚点预设
// 创建 元素的渲染函数 // 创建 元素的渲染函数
this.r = h(OuterNode, { this.r = h(OuterNode, {
model: props.model, model: props.model,
component: Sleep, component: Sleep,
properties: { properties: {
...props.model.getProperties(), ...props.model.getProperties(),
}, },
onContentChange: this.handleContentChange.bind(this), onContentChange: this.handleContentChange.bind(this),
onBindRef: this.handleComponentInstance.bind(this) onBindRef: this.handleComponentInstance.bind(this)
}); });
// 创建 Vue 应用实例,并指定渲染函数 // 创建 Vue 应用实例,并指定渲染函数
this.app = createApp({ this.app = createApp({
render: () => this.r, render: () => this.r,
}); });
} }
/** /**
* HTML 内容设置到指定的根元素上 * HTML 内容设置到指定的根元素上
* @param rootEl 根元素 * @param rootEl 根元素
*/ */
async setHtml(rootEl) { async setHtml(rootEl) {
const nodeId = this.props.model.id; const nodeId = this.props.model.id;
if (SleepNodeHtmlNode.reusePool.has(nodeId)) { if (SleepNodeHtmlNode.reusePool.has(nodeId)) {
rootEl.appendChild(SleepNodeHtmlNode.reusePool.get(nodeId)); rootEl.appendChild(SleepNodeHtmlNode.reusePool.get(nodeId));
return; return;
} }
if (!this.isMounted) { if (!this.isMounted) {
this.isMounted = true; this.isMounted = true;
this.container = document.createElement("div"); this.container = document.createElement("div");
this.container.style.display = "inline-block"; // 关键:确保容器自适应内容 this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
rootEl.appendChild(this.container); rootEl.appendChild(this.container);
this.app.use(ElementPlus); this.app.use(ElementPlus);
this.app.use(JsonViewer); this.app.use(JsonViewer);
this.app.mount(this.container); this.app.mount(this.container);
await nextTick(); await nextTick();
this.setupSizeObserver(); this.setupSizeObserver();
SleepNodeHtmlNode.reusePool.set(nodeId, this.container); SleepNodeHtmlNode.reusePool.set(nodeId, this.container);
} else { } else {
this.r.component.props.properties = this.props.model.getProperties(); this.r.component.props.properties = this.props.model.getProperties();
} }
} }
/** /**
* 获取节点文本内容 * 获取节点文本内容
* 对于元素返回 null因为其内容由特定组件渲染 * 对于元素返回 null因为其内容由特定组件渲染
* @returns {null} * @returns {null}
*/ */
getText() { getText() {
return null; return null;
} }
handleComponentInstance(data) { handleComponentInstance(data) {
// 将组件实例保存到节点模型 // 将组件实例保存到节点模型
this.props.model.setComponentInstance(data) this.props.model.setComponentInstance(data)
} }
handleContentChange() { handleContentChange() {
// 内容变化时强制更新尺寸 // 内容变化时强制更新尺寸
this.updateNodeSize(); this.updateNodeSize();
} }
// 渲染完成后获取实际尺寸 // 渲染完成后获取实际尺寸
updateNodeSize() { updateNodeSize() {
if (this.container) { if (this.container) {
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel; const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
const node = this.container.querySelector(".node__container"); const node = this.container.querySelector(".node__container");
const rect = node.getBoundingClientRect(); const rect = node.getBoundingClientRect();
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y); this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
} }
} }
setupSizeObserver() { setupSizeObserver() {
// 首次渲染立即检测 // 首次渲染立即检测
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.updateNodeSize(); this.updateNodeSize();
// 持续监听变化 // 持续监听变化
this.resizeObserver = new ResizeObserver(() => { this.resizeObserver = new ResizeObserver(() => {
this.updateNodeSize(); this.updateNodeSize();
}); });
this.resizeObserver.observe(this.container); this.resizeObserver.observe(this.container);
}); });
} }
// 组件卸载时移除监听 // 组件卸载时移除监听
onDestroy() { onDestroy() {
this.resizeObserver?.disconnect(); this.resizeObserver?.disconnect();
} }
} }
/** /**
* 定义一个元素的 HTML 模型类继承自 HtmlNodeModel * 定义一个元素的 HTML 模型类继承自 HtmlNodeModel
* 该类主要设置节点的属性和样式 * 该类主要设置节点的属性和样式
*/ */
class SleepNodeHtmlModel extends HtmlNodeModel { class SleepNodeHtmlModel extends HtmlNodeModel {
initNodeData(data) { initNodeData(data) {
super.initNodeData(data); super.initNodeData(data);
} }
// 保存组件实例引用 // 保存组件实例引用
setComponentInstance(instance) { setComponentInstance(instance) {
this.componentInstance = instance; this.componentInstance = instance;
} }
// 验证表单 closePopover() {
async validateForm() { this.componentInstance.closePopover()
const result = this.componentInstance.validateForm() }
return result
} /**
* 设置节点属性
setCustomProperties() { * 包括宽度高度文本编辑属性等
this.componentInstance.setNodeProperties() */
} setAttributes() {
// 初始设置为0后续动态更新
/** this.width = 0;
* 设置节点属性 this.height = 0;
* 包括宽度高度文本编辑属性等 this.text.editable = false;
*/ }
setAttributes() {
// 初始设置为0后续动态更新 updateSize(width, height) {
this.width = 0; this.width = width + 24;
this.height = 0; this.height = height + 50;
this.text.editable = false; this.initNodeData(this); // 触发节点重绘
} }
updateSize(width, height) { // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
this.width = width + 24; getDefaultAnchor() {
this.height = height + 50; let _a = this,
this.initNodeData(this); // 触发节点重绘 x = _a.x,
} y = _a.y,
width = _a.width,
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。 height = _a.height;
getDefaultAnchor() { return [
let _a = this, {
x = _a.x, x: x + width / 2 + 4,
y = _a.y, y: y,
width = _a.width, name: "right",
height = _a.height; id: "".concat(this.id, "_1"),
return [ properties: { connectionType: "source" },
{ },
x: x + width / 2 + 4, {
y: y, x: x - width / 2,
name: "right", y: y,
id: "".concat(this.id, "_1"), name: "left",
properties: { connectionType: "source" }, id: "".concat(this.id, "_3"),
}, properties: { connectionType: "target" },
{ },
x: x - width / 2, ];
y: y, }
name: "left",
id: "".concat(this.id, "_3"), /**
properties: { connectionType: "target" }, * 获取节点轮廓样式
}, * 覆盖父类方法设置 stroke 属性为 none以适应特定的视觉效果
]; * @returns {object} 节点轮廓样式
} */
getOutlineStyle() {
/** const style = super.getOutlineStyle();
* 获取节点轮廓样式 style.stroke = "none";
* 覆盖父类方法设置 stroke 属性为 none以适应特定的视觉效果 style.hover.stroke = "none";
* @returns {object} 节点轮廓样式 return style;
*/ }
getOutlineStyle() { }
const style = super.getOutlineStyle();
style.stroke = "none"; // 导出方法注册
style.hover.stroke = "none"; export function registerSleepNode(lf) {
return style; lf.register({
} type: "sleep",
} view: SleepNodeHtmlNode,
model: SleepNodeHtmlModel
// 导出方法注册 });
export function registerSleepNode(lf) { }
lf.register({
type: "sleep",
view: SleepNodeHtmlNode,
model: SleepNodeHtmlModel
});
}

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="node__container" :class="props.model.id"> <div class="node__container" :class="props.model.id">
<NodeState :state="nodeOperatingStatus" :runtimes="runtimes"> <NodeState :state="nodeOperatingStatus" :runtimes="runtimes" ref="nodeStateRef">
<template #input> <template #input>
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" /> <JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
</template> </template>
@ -33,7 +33,7 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted, onUnmounted } from "vue"; import { ref, onMounted, onUnmounted } from "vue";
import NodeTitle from "../../components/NodeTitle.vue"; import NodeTitle from "../../components/NodeTitle.vue";
import NodeState from "../../components/NodeState.vue"; import NodeState from "../../components/NodeState.vue";
import "vue3-json-viewer/dist/index.css"; import "vue3-json-viewer/dist/index.css";
@ -61,6 +61,14 @@ const runtimes = ref(0);
const inputJsonData = ref({}); const inputJsonData = ref({});
const outputJsonData = ref({}); const outputJsonData = ref({});
const nodeStateRef = ref(null);
const closePopover = () => {
if (nodeStateRef.value) {
nodeStateRef.value.closePopover();
}
}
onMounted(() => { onMounted(() => {
emitter.on("changeNodeState", (data) => { emitter.on("changeNodeState", (data) => {
if (data.nodeId === props.model.id) { if (data.nodeId === props.model.id) {
@ -105,6 +113,8 @@ onUnmounted(() => {
emitter.off("setProperties"); emitter.off("setProperties");
}); });
defineExpose({ closePopover })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,208 +1,191 @@
// 导入 HtmlNode 及其模型,为后续继承做准备 // 导入 HtmlNode 及其模型,为后续继承做准备
import { HtmlNode, HtmlNodeModel } from "@logicflow/core"; import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
// 导入 Vue 相关方法,用于渲染组件 // 导入 Vue 相关方法,用于渲染组件
import { createApp, h, nextTick } from "vue"; import { createApp, h, nextTick } from "vue";
import ElementPlus from "element-plus"; import ElementPlus from "element-plus";
// 导入 Vue 组件 // 导入 Vue 组件
import StopLoop from "./stopLoop.vue"; import StopLoop from "./stopLoop.vue";
import JsonViewer from 'vue3-json-viewer' import JsonViewer from 'vue3-json-viewer'
/** /**
* 定义一个 元素的 HTML 节点类继承自 HtmlNode * 定义一个 元素的 HTML 节点类继承自 HtmlNode
* 该类负责在 HTML 中渲染 元素并处理其交互逻辑 * 该类负责在 HTML 中渲染 元素并处理其交互逻辑
*/ */
class StopLoopHtmlNode extends HtmlNode { class StopLoopHtmlNode extends HtmlNode {
resizeObserver = null; resizeObserver = null;
isMounted; // 标记组件是否已挂载 isMounted; // 标记组件是否已挂载
r; // 渲染函数 r; // 渲染函数
app; // Vue 应用实例 app; // Vue 应用实例
container = null; container = null;
/** /**
* 构造函数 * 构造函数
* @param props 传递给节点的属性包括模型图模型等 * @param props 传递给节点的属性包括模型图模型等
*/ */
constructor(props) { constructor(props) {
super(props); super(props);
this.initVueApp(props); this.initVueApp(props);
} }
initVueApp(props) { initVueApp(props) {
this.isMounted = false; this.isMounted = false;
this.hideAnchor = false; this.hideAnchor = false;
this.autoExpand = true; // 防止锚点被折叠 this.autoExpand = true; // 防止锚点被折叠
this.anchorsPreset = "default"; // 重置锚点预设 this.anchorsPreset = "default"; // 重置锚点预设
// 创建 元素的渲染函数 // 创建 元素的渲染函数
this.r = h(StopLoop, { this.r = h(StopLoop, {
model: props.model, model: props.model,
graphModel: props.graphModel, graphModel: props.graphModel,
properties: { properties: {
...props.model.getProperties(), ...props.model.getProperties(),
}, },
text: props.model.text.value, text: props.model.text.value,
onContentChange: this.handleContentChange.bind(this), onContentChange: this.handleContentChange.bind(this),
onBindRef: this.handleComponentInstance.bind(this), onBindRef: this.handleComponentInstance.bind(this),
}); });
// 创建 Vue 应用实例,并指定渲染函数 // 创建 Vue 应用实例,并指定渲染函数
this.app = createApp({ this.app = createApp({
render: () => this.r, render: () => this.r,
}); });
} }
/** /**
* HTML 内容设置到指定的根元素上 * HTML 内容设置到指定的根元素上
* @param rootEl 根元素 * @param rootEl 根元素
*/ */
async setHtml(rootEl) { async setHtml(rootEl) {
if (!this.isMounted) { if (!this.isMounted) {
this.isMounted = true; this.isMounted = true;
this.container = document.createElement("div"); this.container = document.createElement("div");
this.container.style.display = "inline-block"; // 关键:确保容器自适应内容 this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
rootEl.appendChild(this.container); rootEl.appendChild(this.container);
this.app.use(ElementPlus); // 关键单独注册ElementPlus this.app.use(ElementPlus); // 关键单独注册ElementPlus
this.app.use(JsonViewer) this.app.use(JsonViewer)
this.app.mount(this.container); this.app.mount(this.container);
await nextTick(); await nextTick();
this.setupSizeObserver(); this.setupSizeObserver();
} else { } else {
this.r.component.props.properties = this.props.model.getProperties(); this.r.component.props.properties = this.props.model.getProperties();
} }
} }
/** /**
* 获取节点文本内容 * 获取节点文本内容
* 对于元素返回 null因为其内容由特定组件渲染 * 对于元素返回 null因为其内容由特定组件渲染
* @returns {null} * @returns {null}
*/ */
getText() { getText() {
return null; return null;
} }
handleComponentInstance(data) { handleComponentInstance(data) {
// 将组件实例保存到节点模型 // 将组件实例保存到节点模型
this.props.model.setComponentInstance(data); this.props.model.setComponentInstance(data);
} }
handleContentChange() { handleContentChange() {
// 内容变化时强制更新尺寸 // 内容变化时强制更新尺寸
this.updateNodeSize(); this.updateNodeSize();
} }
// 渲染完成后获取实际尺寸 // 渲染完成后获取实际尺寸
updateNodeSize() { updateNodeSize() {
if (this.container) { if (this.container) {
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel; const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
const node = this.container.querySelector(".node__container"); const node = this.container.querySelector(".node__container");
const rect = node.getBoundingClientRect(); const rect = node.getBoundingClientRect();
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y); this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
} }
} }
setupSizeObserver() { setupSizeObserver() {
// 首次渲染立即检测 // 首次渲染立即检测
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.updateNodeSize(); this.updateNodeSize();
// 持续监听变化 // 持续监听变化
this.resizeObserver = new ResizeObserver(() => { this.resizeObserver = new ResizeObserver(() => {
this.updateNodeSize(); this.updateNodeSize();
}); });
this.resizeObserver.observe(this.container); this.resizeObserver.observe(this.container);
}); });
} }
// 组件卸载时移除监听 // 组件卸载时移除监听
onDestroy() { onDestroy() {
this.resizeObserver?.disconnect(); this.resizeObserver?.disconnect();
} }
} }
/** /**
* 定义一个元素的 HTML 模型类继承自 HtmlNodeModel * 定义一个元素的 HTML 模型类继承自 HtmlNodeModel
* 该类主要设置节点的属性和样式 * 该类主要设置节点的属性和样式
*/ */
class StopLoopHtmlModel extends HtmlNodeModel { class StopLoopHtmlModel extends HtmlNodeModel {
initNodeData(data) { initNodeData(data) {
super.initNodeData(data); super.initNodeData(data);
} }
// 保存组件实例引用 // 保存组件实例引用
setComponentInstance(instance) { setComponentInstance(instance) {
this.componentInstance = instance; this.componentInstance = instance;
} }
// 验证表单 /**
async validateForm() { * 设置节点属性
if (this.componentInstance && this.componentInstance.validate) { * 包括宽度高度文本编辑属性等
try { */
const result = await this.componentInstance.validate(); setAttributes() {
if (result) { // 初始设置为0后续动态更新
return { valid: true, message: "验证通过" }; this.width = 0;
} else { this.height = 0;
return { valid: false, message: "验证失败" }; this.text.editable = false;
} }
} catch {
return { valid: false, message: "验证失败" }; updateSize(width, height) {
} this.width = width + 24;
} this.height = height + 50;
return { valid: true, message: "无验证方法" }; this.initNodeData(this); // 触发节点重绘
} }
/** // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
* 设置节点属性 getDefaultAnchor() {
* 包括宽度高度文本编辑属性等 let _a = this,
*/ x = _a.x,
setAttributes() { y = _a.y,
// 初始设置为0后续动态更新 width = _a.width,
this.width = 0; height = _a.height;
this.height = 0; return [
this.text.editable = false; {
} x: x - width / 2,
y: y - 25,
updateSize(width, height) { name: "left",
this.width = width + 24; id: "".concat(this.id, "_3"),
this.height = height + 50; properties: { connectionType: "target" },
this.initNodeData(this); // 触发节点重绘 },
} ];
}
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
getDefaultAnchor() { /**
let _a = this, * 获取节点轮廓样式
x = _a.x, * 覆盖父类方法设置 stroke 属性为 none以适应特定的视觉效果
y = _a.y, * @returns {object} 节点轮廓样式
width = _a.width, */
height = _a.height; getOutlineStyle() {
return [ const style = super.getOutlineStyle();
{ style.stroke = "none";
x: x - width / 2, style.hover.stroke = "none";
y: y - 25, return style;
name: "left", }
id: "".concat(this.id, "_3"), }
properties: { connectionType: "target" },
}, // 导出方法注册
]; export function registerStopLoopNode(lf) {
} lf.register({
type: "stopLoop",
/** view: StopLoopHtmlNode,
* 获取节点轮廓样式 model: StopLoopHtmlModel,
* 覆盖父类方法设置 stroke 属性为 none以适应特定的视觉效果 });
* @returns {object} 节点轮廓样式 }
*/
getOutlineStyle() {
const style = super.getOutlineStyle();
style.stroke = "none";
style.hover.stroke = "none";
return style;
}
}
// 导出方法注册
export function registerStopLoopNode(lf) {
lf.register({
type: "stopLoop",
view: StopLoopHtmlNode,
model: StopLoopHtmlModel,
});
}

View File

@ -1,203 +1,186 @@
// 导入 HtmlNode 及其模型,为后续继承做准备 // 导入 HtmlNode 及其模型,为后续继承做准备
import { HtmlNode, HtmlNodeModel } from "@logicflow/core"; import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
// 导入 Vue 相关方法,用于渲染组件 // 导入 Vue 相关方法,用于渲染组件
import { createApp, h, nextTick } from "vue"; import { createApp, h, nextTick } from "vue";
import ElementPlus from "element-plus"; import ElementPlus from "element-plus";
// 导入 Vue 组件 // 导入 Vue 组件
import SubEndNode from "./subEnd.vue"; import SubEndNode from "./subEnd.vue";
/** /**
* 定义一个 元素的 HTML 节点类继承自 HtmlNode * 定义一个 元素的 HTML 节点类继承自 HtmlNode
* 该类负责在 HTML 中渲染 元素并处理其交互逻辑 * 该类负责在 HTML 中渲染 元素并处理其交互逻辑
*/ */
class SubEndNodeHtmlNode extends HtmlNode { class SubEndNodeHtmlNode extends HtmlNode {
resizeObserver = null; resizeObserver = null;
isMounted; // 标记组件是否已挂载 isMounted; // 标记组件是否已挂载
r; // 渲染函数 r; // 渲染函数
app; // Vue 应用实例 app; // Vue 应用实例
container = null; container = null;
/** /**
* 构造函数 * 构造函数
* @param props 传递给节点的属性包括模型图模型等 * @param props 传递给节点的属性包括模型图模型等
*/ */
constructor(props) { constructor(props) {
super(props); super(props);
this.initVueApp(props); this.initVueApp(props);
} }
initVueApp(props) { initVueApp(props) {
this.isMounted = false; this.isMounted = false;
this.hideAnchor = false; this.hideAnchor = false;
this.autoExpand = true; // 防止锚点被折叠 this.autoExpand = true; // 防止锚点被折叠
this.anchorsPreset = "default"; // 重置锚点预设 this.anchorsPreset = "default"; // 重置锚点预设
// 创建 元素的渲染函数 // 创建 元素的渲染函数
this.r = h(SubEndNode, { this.r = h(SubEndNode, {
model: props.model, model: props.model,
graphModel: props.graphModel, graphModel: props.graphModel,
properties: { properties: {
...props.model.getProperties(), ...props.model.getProperties(),
} }
}); });
// 创建 Vue 应用实例,并指定渲染函数 // 创建 Vue 应用实例,并指定渲染函数
this.app = createApp({ this.app = createApp({
render: () => this.r, render: () => this.r,
}); });
} }
/** /**
* HTML 内容设置到指定的根元素上 * HTML 内容设置到指定的根元素上
* @param rootEl 根元素 * @param rootEl 根元素
*/ */
async setHtml(rootEl) { async setHtml(rootEl) {
if (!this.isMounted) { if (!this.isMounted) {
this.isMounted = true; this.isMounted = true;
this.container = document.createElement("div"); this.container = document.createElement("div");
this.container.style.display = "inline-block"; // 关键:确保容器自适应内容 this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
rootEl.appendChild(this.container); rootEl.appendChild(this.container);
this.app.use(ElementPlus); // 关键单独注册ElementPlus this.app.use(ElementPlus); // 关键单独注册ElementPlus
this.app.mount(this.container); this.app.mount(this.container);
await nextTick(); await nextTick();
this.setupSizeObserver(); this.setupSizeObserver();
} else { } else {
this.r.component.props.properties = this.props.model.getProperties(); this.r.component.props.properties = this.props.model.getProperties();
} }
} }
/** /**
* 获取节点文本内容 * 获取节点文本内容
* 对于元素返回 null因为其内容由特定组件渲染 * 对于元素返回 null因为其内容由特定组件渲染
* @returns {null} * @returns {null}
*/ */
getText() { getText() {
return null; return null;
} }
handleComponentInstance(data) { handleComponentInstance(data) {
// 将组件实例保存到节点模型 // 将组件实例保存到节点模型
this.props.model.setComponentInstance(data); this.props.model.setComponentInstance(data);
} }
handleContentChange() { handleContentChange() {
// 内容变化时强制更新尺寸 // 内容变化时强制更新尺寸
this.updateNodeSize(); this.updateNodeSize();
} }
// 渲染完成后获取实际尺寸 // 渲染完成后获取实际尺寸
updateNodeSize() { updateNodeSize() {
if (this.container) { if (this.container) {
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel; const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
const node = this.container.querySelector(".node__container"); const node = this.container.querySelector(".node__container");
const rect = node.getBoundingClientRect(); const rect = node.getBoundingClientRect();
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y); this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
} }
} }
setupSizeObserver() { setupSizeObserver() {
// 首次渲染立即检测 // 首次渲染立即检测
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.updateNodeSize(); this.updateNodeSize();
// 持续监听变化 // 持续监听变化
this.resizeObserver = new ResizeObserver(() => { this.resizeObserver = new ResizeObserver(() => {
this.updateNodeSize(); this.updateNodeSize();
}); });
this.resizeObserver.observe(this.container); this.resizeObserver.observe(this.container);
}); });
} }
// 组件卸载时移除监听 // 组件卸载时移除监听
onDestroy() { onDestroy() {
this.resizeObserver?.disconnect(); this.resizeObserver?.disconnect();
} }
} }
/** /**
* 定义一个元素的 HTML 模型类继承自 HtmlNodeModel * 定义一个元素的 HTML 模型类继承自 HtmlNodeModel
* 该类主要设置节点的属性和样式 * 该类主要设置节点的属性和样式
*/ */
class SubEndNodeHtmlModel extends HtmlNodeModel { class SubEndNodeHtmlModel extends HtmlNodeModel {
initNodeData(data) { initNodeData(data) {
super.initNodeData(data); super.initNodeData(data);
} }
// 保存组件实例引用 // 保存组件实例引用
setComponentInstance(instance) { setComponentInstance(instance) {
this.componentInstance = instance; this.componentInstance = instance;
} }
// 验证表单 /**
async validateForm() { * 设置节点属性
if (this.componentInstance && this.componentInstance.validate) { * 包括宽度高度文本编辑属性等
try { */
const result = await this.componentInstance.validate(); setAttributes() {
if (result) { // 初始设置为0后续动态更新
return { valid: true, message: "验证通过" }; this.width = 0;
} else { this.height = 0;
return { valid: false, message: "验证失败" }; this.text.editable = false;
} }
} catch {
return { valid: false, message: "验证失败" }; updateSize(width, height) {
} this.width = width + 24;
} this.height = height + 50;
return { valid: true, message: "无验证方法" }; this.initNodeData(this); // 触发节点重绘
} }
/** // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
* 设置节点属性 getDefaultAnchor() {
* 包括宽度高度文本编辑属性等 let _a = this,
*/ x = _a.x,
setAttributes() { y = _a.y,
// 初始设置为0后续动态更新 width = _a.width,
this.width = 0; height = _a.height;
this.height = 0; return [
this.text.editable = false; {
} x: x - width / 2,
y: y - 25,
updateSize(width, height) { name: "left",
this.width = width + 24; id: "".concat(this.id, "_3"),
this.height = height + 50; properties: { connectionType: "target" },
this.initNodeData(this); // 触发节点重绘 },
} ];
}
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
getDefaultAnchor() { /**
let _a = this, * 获取节点轮廓样式
x = _a.x, * 覆盖父类方法设置 stroke 属性为 none以适应特定的视觉效果
y = _a.y, * @returns {object} 节点轮廓样式
width = _a.width, */
height = _a.height; getOutlineStyle() {
return [ const style = super.getOutlineStyle();
{ style.stroke = "none";
x: x - width / 2, style.hover.stroke = "none";
y: y - 25, return style;
name: "left", }
id: "".concat(this.id, "_3"), }
properties: { connectionType: "target" },
}, // 导出方法注册
]; export function registerSubEndNode(lf) {
} lf.register({
type: "subEnd",
/** view: SubEndNodeHtmlNode,
* 获取节点轮廓样式 model: SubEndNodeHtmlModel,
* 覆盖父类方法设置 stroke 属性为 none以适应特定的视觉效果 });
* @returns {object} 节点轮廓样式 }
*/
getOutlineStyle() {
const style = super.getOutlineStyle();
style.stroke = "none";
style.hover.stroke = "none";
return style;
}
}
// 导出方法注册
export function registerSubEndNode(lf) {
lf.register({
type: "subEnd",
view: SubEndNodeHtmlNode,
model: SubEndNodeHtmlModel,
});
}

View File

@ -1,202 +1,185 @@
// 导入 HtmlNode 及其模型,为后续继承做准备 // 导入 HtmlNode 及其模型,为后续继承做准备
import { HtmlNode, HtmlNodeModel } from "@logicflow/core"; import { HtmlNode, HtmlNodeModel } from "@logicflow/core";
// 导入 Vue 相关方法,用于渲染组件 // 导入 Vue 相关方法,用于渲染组件
import { createApp, h, nextTick } from "vue"; import { createApp, h, nextTick } from "vue";
import ElementPlus from "element-plus"; import ElementPlus from "element-plus";
// 导入 Vue 组件 // 导入 Vue 组件
import SubStartNode from "./subStart.vue"; import SubStartNode from "./subStart.vue";
/** /**
* 定义一个 元素的 HTML 节点类继承自 HtmlNode * 定义一个 元素的 HTML 节点类继承自 HtmlNode
* 该类负责在 HTML 中渲染 元素并处理其交互逻辑 * 该类负责在 HTML 中渲染 元素并处理其交互逻辑
*/ */
class SubStartNodeHtmlNode extends HtmlNode { class SubStartNodeHtmlNode extends HtmlNode {
resizeObserver = null; resizeObserver = null;
isMounted; // 标记组件是否已挂载 isMounted; // 标记组件是否已挂载
r; // 渲染函数 r; // 渲染函数
app; // Vue 应用实例 app; // Vue 应用实例
container = null; container = null;
/** /**
* 构造函数 * 构造函数
* @param props 传递给节点的属性包括模型图模型等 * @param props 传递给节点的属性包括模型图模型等
*/ */
constructor(props) { constructor(props) {
super(props); super(props);
this.initVueApp(props); this.initVueApp(props);
} }
initVueApp(props) { initVueApp(props) {
this.isMounted = false; this.isMounted = false;
this.hideAnchor = false; this.hideAnchor = false;
this.autoExpand = true; // 防止锚点被折叠 this.autoExpand = true; // 防止锚点被折叠
this.anchorsPreset = "default"; // 重置锚点预设 this.anchorsPreset = "default"; // 重置锚点预设
// 创建 元素的渲染函数 // 创建 元素的渲染函数
this.r = h(SubStartNode, { this.r = h(SubStartNode, {
model: props.model, model: props.model,
graphModel: props.graphModel, graphModel: props.graphModel,
properties: { properties: {
...props.model.getProperties(), ...props.model.getProperties(),
} }
}); });
// 创建 Vue 应用实例,并指定渲染函数 // 创建 Vue 应用实例,并指定渲染函数
this.app = createApp({ this.app = createApp({
render: () => this.r, render: () => this.r,
}); });
} }
/** /**
* HTML 内容设置到指定的根元素上 * HTML 内容设置到指定的根元素上
* @param rootEl 根元素 * @param rootEl 根元素
*/ */
async setHtml(rootEl) { async setHtml(rootEl) {
if (!this.isMounted) { if (!this.isMounted) {
this.isMounted = true; this.isMounted = true;
this.container = document.createElement("div"); this.container = document.createElement("div");
this.container.style.display = "inline-block"; // 关键:确保容器自适应内容 this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
rootEl.appendChild(this.container); rootEl.appendChild(this.container);
this.app.use(ElementPlus); // 关键单独注册ElementPlus this.app.use(ElementPlus); // 关键单独注册ElementPlus
this.app.mount(this.container); this.app.mount(this.container);
await nextTick(); await nextTick();
this.setupSizeObserver(); this.setupSizeObserver();
} else { } else {
this.r.component.props.properties = this.props.model.getProperties(); this.r.component.props.properties = this.props.model.getProperties();
} }
} }
/** /**
* 获取节点文本内容 * 获取节点文本内容
* 对于元素返回 null因为其内容由特定组件渲染 * 对于元素返回 null因为其内容由特定组件渲染
* @returns {null} * @returns {null}
*/ */
getText() { getText() {
return null; return null;
} }
handleComponentInstance(data) { handleComponentInstance(data) {
// 将组件实例保存到节点模型 // 将组件实例保存到节点模型
this.props.model.setComponentInstance(data); this.props.model.setComponentInstance(data);
} }
handleContentChange() { handleContentChange() {
// 内容变化时强制更新尺寸 // 内容变化时强制更新尺寸
this.updateNodeSize(); this.updateNodeSize();
} }
// 渲染完成后获取实际尺寸 // 渲染完成后获取实际尺寸
updateNodeSize() { updateNodeSize() {
if (this.container) { if (this.container) {
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel; const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
const node = this.container.querySelector(".node__container"); const node = this.container.querySelector(".node__container");
const rect = node.getBoundingClientRect(); const rect = node.getBoundingClientRect();
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y); this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
} }
} }
setupSizeObserver() { setupSizeObserver() {
// 首次渲染立即检测 // 首次渲染立即检测
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.updateNodeSize(); this.updateNodeSize();
// 持续监听变化 // 持续监听变化
this.resizeObserver = new ResizeObserver(() => { this.resizeObserver = new ResizeObserver(() => {
this.updateNodeSize(); this.updateNodeSize();
}); });
this.resizeObserver.observe(this.container); this.resizeObserver.observe(this.container);
}); });
} }
// 组件卸载时移除监听 // 组件卸载时移除监听
onDestroy() { onDestroy() {
this.resizeObserver?.disconnect(); this.resizeObserver?.disconnect();
} }
} }
/** /**
* 定义一个元素的 HTML 模型类继承自 HtmlNodeModel * 定义一个元素的 HTML 模型类继承自 HtmlNodeModel
* 该类主要设置节点的属性和样式 * 该类主要设置节点的属性和样式
*/ */
class SubStartNodeHtmlModel extends HtmlNodeModel { class SubStartNodeHtmlModel extends HtmlNodeModel {
initNodeData(data) { initNodeData(data) {
super.initNodeData(data); super.initNodeData(data);
} }
// 保存组件实例引用 // 保存组件实例引用
setComponentInstance(instance) { setComponentInstance(instance) {
this.componentInstance = instance; this.componentInstance = instance;
} }
// 验证表单 /**
async validateForm() { * 设置节点属性
if (this.componentInstance && this.componentInstance.validate) { * 包括宽度高度文本编辑属性等
try { */
const result = await this.componentInstance.validate(); setAttributes() {
if (result) { // 初始设置为0后续动态更新
return { valid: true, message: "验证通过" }; this.width = 0;
} else { this.height = 0;
return { valid: false, message: "验证失败" }; this.text.editable = false;
} }
} catch {
return { valid: false, message: "验证失败" }; updateSize(width, height) {
} this.width = width + 24;
} this.height = height + 50;
return { valid: true, message: "无验证方法" }; this.initNodeData(this); // 触发节点重绘
} }
/** // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
* 设置节点属性 getDefaultAnchor() {
* 包括宽度高度文本编辑属性等 let _a = this,
*/ x = _a.x,
setAttributes() { y = _a.y,
// 初始设置为0后续动态更新 width = _a.width,
this.width = 0; height = _a.height;
this.height = 0; return [
this.text.editable = false; {
} x: x + width / 2 - 24,
y: y - 25,
updateSize(width, height) { name: "right",
this.width = width + 24; id: "".concat(this.id, "_1")
this.height = height + 50; },
this.initNodeData(this); // 触发节点重绘 ];
} }
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。 /**
getDefaultAnchor() { * 获取节点轮廓样式
let _a = this, * 覆盖父类方法设置 stroke 属性为 none以适应特定的视觉效果
x = _a.x, * @returns {object} 节点轮廓样式
y = _a.y, */
width = _a.width, getOutlineStyle() {
height = _a.height; const style = super.getOutlineStyle();
return [ style.stroke = "none";
{ style.hover.stroke = "none";
x: x + width / 2 - 24, return style;
y: y - 25, }
name: "right", }
id: "".concat(this.id, "_1")
}, // 导出方法注册
]; export function registerSubStartNode(lf) {
} lf.register({
type: "subStart",
/** view: SubStartNodeHtmlNode,
* 获取节点轮廓样式 model: SubStartNodeHtmlModel,
* 覆盖父类方法设置 stroke 属性为 none以适应特定的视觉效果 });
* @returns {object} 节点轮廓样式 }
*/
getOutlineStyle() {
const style = super.getOutlineStyle();
style.stroke = "none";
style.hover.stroke = "none";
return style;
}
}
// 导出方法注册
export function registerSubStartNode(lf) {
lf.register({
type: "subStart",
view: SubStartNodeHtmlNode,
model: SubStartNodeHtmlModel,
});
}

View File

@ -1,314 +1,318 @@
// 导入 HtmlNode 及其模型,为后续继承做准备 // 导入 HtmlNode 及其模型,为后续继承做准备
import { HtmlNode, HtmlNodeModel, h as svgH } from "@logicflow/core"; import { HtmlNode, HtmlNodeModel, h as svgH } from "@logicflow/core";
// 导入 Vue 相关方法,用于渲染组件 // 导入 Vue 相关方法,用于渲染组件
import { createApp, h, nextTick } from "vue"; import { createApp, h, nextTick } from "vue";
import ElementPlus from "element-plus"; import ElementPlus from "element-plus";
// 导入 Vue 组件 // 导入 Vue 组件
import Switch from "./switch.vue"; import Switch from "./switch.vue";
import JsonViewer from 'vue3-json-viewer' import JsonViewer from 'vue3-json-viewer'
/** /**
* 定义一个 元素的 HTML 节点类继承自 HtmlNode * 定义一个 元素的 HTML 节点类继承自 HtmlNode
* 该类负责在 HTML 中渲染 元素并处理其交互逻辑 * 该类负责在 HTML 中渲染 元素并处理其交互逻辑
*/ */
class SwitchHtmlNode extends HtmlNode { class SwitchHtmlNode extends HtmlNode {
resizeObserver = null; resizeObserver = null;
isMounted; // 标记组件是否已挂载 isMounted; // 标记组件是否已挂载
r; // 渲染函数 r; // 渲染函数
app; // Vue 应用实例 app; // Vue 应用实例
container = null; container = null;
static reusePool = new Map(); // 节点复用池 static reusePool = new Map(); // 节点复用池
/** /**
* 构造函数 * 构造函数
* @param props 传递给节点的属性包括模型图模型等 * @param props 传递给节点的属性包括模型图模型等
*/ */
constructor(props) { constructor(props) {
super(props); super(props);
this.initVueApp(props); this.initVueApp(props);
} }
initVueApp(props) { initVueApp(props) {
this.isMounted = false; this.isMounted = false;
this.hideAnchor = false; this.hideAnchor = false;
this.autoExpand = true; // 防止锚点被折叠 this.autoExpand = true; // 防止锚点被折叠
this.anchorsPreset = "default"; // 重置锚点预设 this.anchorsPreset = "default"; // 重置锚点预设
// 创建 元素的渲染函数 // 创建 元素的渲染函数
this.r = h(Switch, { this.r = h(Switch, {
model: props.model, model: props.model,
graphModel: props.graphModel, graphModel: props.graphModel,
properties: { properties: {
...props.model.getProperties(), ...props.model.getProperties(),
}, },
text: props.model.text.value, text: props.model.text.value,
onContentChange: this.handleContentChange.bind(this), onContentChange: this.handleContentChange.bind(this),
onBindRef: this.handleComponentInstance.bind(this), onBindRef: this.handleComponentInstance.bind(this),
// 添加锚点相关回调 // 添加锚点相关回调
onAddAnchor: this.handleAddAnchor.bind(this), onAddAnchor: this.handleAddAnchor.bind(this),
onRemoveAnchor: this.handleRemoveAnchor.bind(this), onRemoveAnchor: this.handleRemoveAnchor.bind(this),
onChangeAnchor: this.handleChangeAnchor.bind(this) onChangeAnchor: this.handleChangeAnchor.bind(this)
}); });
// 创建 Vue 应用实例,并指定渲染函数 // 创建 Vue 应用实例,并指定渲染函数
this.app = createApp({ this.app = createApp({
render: () => this.r, render: () => this.r,
}); });
} }
/** /**
* HTML 内容设置到指定的根元素上 * HTML 内容设置到指定的根元素上
* @param rootEl 根元素 * @param rootEl 根元素
*/ */
async setHtml(rootEl) { async setHtml(rootEl) {
const nodeId = this.props.model.id; const nodeId = this.props.model.id;
if (SwitchHtmlNode.reusePool.has(nodeId)) { if (SwitchHtmlNode.reusePool.has(nodeId)) {
rootEl.appendChild(SwitchHtmlNode.reusePool.get(nodeId)); rootEl.appendChild(SwitchHtmlNode.reusePool.get(nodeId));
return; return;
} }
if (!this.isMounted) { if (!this.isMounted) {
this.isMounted = true; this.isMounted = true;
this.container = document.createElement("div"); this.container = document.createElement("div");
this.container.style.display = "inline-block"; // 关键:确保容器自适应内容 this.container.style.display = "inline-block"; // 关键:确保容器自适应内容
rootEl.appendChild(this.container); rootEl.appendChild(this.container);
this.app.use(ElementPlus); this.app.use(ElementPlus);
this.app.use(JsonViewer); this.app.use(JsonViewer);
this.app.mount(this.container); this.app.mount(this.container);
await nextTick(); await nextTick();
this.setupSizeObserver(); this.setupSizeObserver();
SwitchHtmlNode.reusePool.set(nodeId, this.container); SwitchHtmlNode.reusePool.set(nodeId, this.container);
} else { } else {
this.r.component.props.properties = this.props.model.getProperties(); this.r.component.props.properties = this.props.model.getProperties();
} }
} }
/** /**
* 获取节点文本内容 * 获取节点文本内容
* 对于元素返回 null因为其内容由特定组件渲染 * 对于元素返回 null因为其内容由特定组件渲染
* @returns {null} * @returns {null}
*/ */
getText() { getText() {
return null; return null;
} }
handleComponentInstance(data) { handleComponentInstance(data) {
// 将组件实例保存到节点模型 // 将组件实例保存到节点模型
this.props.model.setComponentInstance(data) this.props.model.setComponentInstance(data)
} }
handleContentChange() { handleContentChange() {
// 内容变化时强制更新尺寸 // 内容变化时强制更新尺寸
this.updateNodeSize(); this.updateNodeSize();
} }
// 处理添加锚点 // 处理添加锚点
handleAddAnchor(height, anchorId) { handleAddAnchor(height, anchorId) {
const { model } = this.props const { model } = this.props
model.addAnchor(height, anchorId) model.addAnchor(height, anchorId)
} }
// 处理删除锚点 // 处理删除锚点
handleRemoveAnchor(anchorId) { handleRemoveAnchor(anchorId) {
this.props.model.removeAnchor(anchorId); this.props.model.removeAnchor(anchorId);
} }
handleChangeAnchor(height, anchorId) { handleChangeAnchor(height, anchorId) {
const { model } = this.props const { model } = this.props
model.changeAnchorHeight(height, anchorId) model.changeAnchorHeight(height, anchorId)
} }
// 渲染完成后获取实际尺寸 // 渲染完成后获取实际尺寸
updateNodeSize() { updateNodeSize() {
if (this.container) { if (this.container) {
const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel; const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel;
const node = this.container.querySelector(".node__container"); const node = this.container.querySelector(".node__container");
const rect = node.getBoundingClientRect(); const rect = node.getBoundingClientRect();
this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y); this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y);
} }
} }
setupSizeObserver() { setupSizeObserver() {
// 首次渲染立即检测 // 首次渲染立即检测
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.updateNodeSize(); this.updateNodeSize();
// 持续监听变化 // 持续监听变化
this.resizeObserver = new ResizeObserver(() => { this.resizeObserver = new ResizeObserver(() => {
this.updateNodeSize(); this.updateNodeSize();
}); });
this.resizeObserver.observe(this.container); this.resizeObserver.observe(this.container);
}); });
} }
// 组件卸载时移除监听 // 组件卸载时移除监听
onDestroy() { onDestroy() {
this.resizeObserver?.disconnect(); this.resizeObserver?.disconnect();
} }
} }
/** /**
* 定义一个元素的 HTML 模型类继承自 HtmlNodeModel * 定义一个元素的 HTML 模型类继承自 HtmlNodeModel
* 该类主要设置节点的属性和样式 * 该类主要设置节点的属性和样式
*/ */
class SwitchHtmlModel extends HtmlNodeModel { class SwitchHtmlModel extends HtmlNodeModel {
initNodeData(data) { initNodeData(data) {
super.initNodeData(data); super.initNodeData(data);
} }
// 返回动态锚点 // 返回动态锚点
addAnchor(height, anchorId) { addAnchor(height, anchorId) {
const properties = this.getProperties() const properties = this.getProperties()
const anchor = properties?.anchor || [] const anchor = properties?.anchor || []
anchor.push({ anchor.push({
id: anchorId, id: anchorId,
name: 'right', name: 'right',
x: this.x, x: this.x,
y: this.y, y: this.y,
height height
}) })
this.setProperties({ this.setProperties({
...properties, ...properties,
anchor anchor
}) })
this.getDefaultAnchor() this.getDefaultAnchor()
this.initNodeData(this) this.initNodeData(this)
} }
removeAnchor(anchorId) { removeAnchor(anchorId) {
const properties = this.getProperties() const properties = this.getProperties()
const anchor = properties?.anchor || [] const anchor = properties?.anchor || []
let nweAnchor = anchor.filter(item => item.id !== anchorId); let nweAnchor = anchor.filter(item => item.id !== anchorId);
this.setProperties({ this.setProperties({
...properties, ...properties,
anchor: nweAnchor anchor: nweAnchor
}) })
} }
changeAnchorHeight(height, anchorId) { changeAnchorHeight(height, anchorId) {
const properties = this.getProperties() const properties = this.getProperties()
const anchor = properties?.anchor || [] const anchor = properties?.anchor || []
anchor.forEach(item => { anchor.forEach(item => {
if (item.id === anchorId) { if (item.id === anchorId) {
item.height = height item.height = height
} }
}) })
this.setProperties({ this.setProperties({
...properties, ...properties,
anchor anchor
}) })
this.getDefaultAnchor() this.getDefaultAnchor()
this.initNodeData(this) this.initNodeData(this)
} }
// 保存组件实例引用 // 保存组件实例引用
setComponentInstance(instance) { setComponentInstance(instance) {
this.componentInstance = instance; this.componentInstance = instance;
} }
// 验证表单 closePopover() {
async validateForm() { this.componentInstance.closePopover()
if (this.componentInstance && this.componentInstance.validate) { }
try {
const result = await this.componentInstance.validate(); // 验证表单
if (result) { async validateForm() {
return { valid: true, message: "验证通过" }; if (this.componentInstance && this.componentInstance.validate) {
} else { try {
return { valid: false, message: "验证失败" }; const result = await this.componentInstance.validate();
} if (result) {
} catch { return { valid: true, message: "验证通过" };
return { valid: false, message: "验证失败" }; } else {
} return { valid: false, message: "验证失败" };
} }
return { valid: true, message: "无验证方法" }; } catch {
} return { valid: false, message: "验证失败" };
}
/** }
* 设置节点属性 return { valid: true, message: "无验证方法" };
* 包括宽度高度文本编辑属性等 }
*/
setAttributes() { /**
// 初始设置为0后续动态更新 * 设置节点属性
this.width = 0; * 包括宽度高度文本编辑属性等
this.height = 0; */
this.text.editable = false; setAttributes() {
} // 初始设置为0后续动态更新
this.width = 0;
updateSize(width, height) { this.height = 0;
this.width = width + 24; this.text.editable = false;
this.height = height + 50; }
// const properties = lf.getProperties(this.id)
// this.setProperties({ updateSize(width, height) {
// ...properties, this.width = width + 24;
// width: this.width, this.height = height + 50;
// height: this.height // const properties = lf.getProperties(this.id)
// }) // this.setProperties({
this.initNodeData(this); // 触发节点重绘 // ...properties,
} // width: this.width,
// height: this.height
// 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。 // })
getDefaultAnchor() { this.initNodeData(this); // 触发节点重绘
let _a = this, }
x = _a.x,
y = _a.y, // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。
width = _a.width, getDefaultAnchor() {
height = _a.height; let _a = this,
const properties = this.getProperties() x = _a.x,
const anchor = properties?.anchor || [] y = _a.y,
// const { SCALE_Y } = this.graphModel.transformModel; width = _a.width,
const ids = [] height = _a.height;
anchor.forEach(item => { const properties = this.getProperties()
ids.push(item.id) const anchor = properties?.anchor || []
if (item.id !== "".concat(this.id, "_else")) { // const { SCALE_Y } = this.graphModel.transformModel;
item.x = x + width / 2 - 40 const ids = []
// item.y = (y - height / 2 + item.height + 60) / SCALE_Y anchor.forEach(item => {
item.y = y - height / 2 + item.height + 60 ids.push(item.id)
} else { if (item.id !== "".concat(this.id, "_else")) {
item.x = x + width / 2 - 40 item.x = x + width / 2 - 40
item.y = y + height / 2 - 80 // item.y = (y - height / 2 + item.height + 60) / SCALE_Y
} item.y = y - height / 2 + item.height + 60
}) } else {
item.x = x + width / 2 - 40
return [ item.y = y + height / 2 - 80
// { }
// x: x + width / 2 - 40, })
// y: y + height / 2 - 80,
// name: "right", return [
// id: "".concat(this.id, "_else") // {
// }, // x: x + width / 2 - 40,
{ // y: y + height / 2 - 80,
x: x - width / 2, // name: "right",
y: y - 25, // id: "".concat(this.id, "_else")
name: "left", // },
id: "".concat(this.id, "_entry") {
}, x: x - width / 2,
...anchor y: y - 25,
]; name: "left",
} id: "".concat(this.id, "_entry")
},
/** ...anchor
* 获取节点轮廓样式 ];
* 覆盖父类方法设置 stroke 属性为 none以适应特定的视觉效果 }
* @returns {object} 节点轮廓样式
*/ /**
getOutlineStyle() { * 获取节点轮廓样式
const style = super.getOutlineStyle(); * 覆盖父类方法设置 stroke 属性为 none以适应特定的视觉效果
style.stroke = "none"; * @returns {object} 节点轮廓样式
style.hover.stroke = "none"; */
return style; getOutlineStyle() {
} const style = super.getOutlineStyle();
} style.stroke = "none";
style.hover.stroke = "none";
// 导出方法注册 return style;
export function registerSwitch(lf) { }
lf.register({ }
type: "branch",
view: SwitchHtmlNode, // 导出方法注册
model: SwitchHtmlModel, export function registerSwitch(lf) {
}); lf.register({
} type: "branch",
view: SwitchHtmlNode,
model: SwitchHtmlModel,
});
}

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="node__container" ref="switchRef" @mouseleave="setNodeProperties"> <div class="node__container" ref="switchRef" @mouseleave="setNodeProperties">
<NodeState :state="nodeOperatingStatus" :runtimes="runtimes"> <NodeState :state="nodeOperatingStatus" :runtimes="runtimes" ref="nodeStateRef">
<template #input> <template #input>
<JsonViewer :value="inputJsonData" copyable boxed sort theme="light" /> <JsonViewer :value="inputJsonData" copyable boxed sort theme="light" />
</template> </template>
@ -407,6 +407,14 @@ watch(
} }
); );
const nodeStateRef = ref(null);
const closePopover = () => {
if (nodeStateRef.value) {
nodeStateRef.value.closePopover();
}
}
onMounted(() => { onMounted(() => {
emits("addAnchor", 280, "".concat(props.model.id, "_else")); emits("addAnchor", 280, "".concat(props.model.id, "_else"));
@ -450,6 +458,8 @@ onUnmounted(() => {
emitter.off("changeNodeState"); emitter.off("changeNodeState");
emitter.off("contentChange"); emitter.off("contentChange");
}); });
defineExpose({ closePopover })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.node__container { .node__container {

View File

@ -51,7 +51,7 @@ export default defineConfig(({mode, command}) => {
//李小龙 http://192.168.0.5:13080 //李小龙 http://192.168.0.5:13080
// dev http://10.148.20.34:13080 // dev http://10.148.20.34:13080
// target: VITE_API_URL, // target: VITE_API_URL,
target: command === 'build' ? VITE_API_URL : 'http://192.168.0.5:13080', target: command === 'build' ? VITE_API_URL : 'http://192.168.0.100:13080',
changeOrigin: true, changeOrigin: true,
rewrite: (p) => p.replace(/^\/dev-api/, '') rewrite: (p) => p.replace(/^\/dev-api/, '')
} }