Vue3中使用LogicFlow实现简单流程图

实现结果

实现功能:

  1. 拖拽创建节点
  2. 自定义节点/边
  3. 自定义快捷键
  4. 人员选择弹窗
  5. 右侧动态配置组件
  6. 配置项获取/回显
  7. 必填项验证
  8. 历史记录(撤销/恢复)

自定义节点与拖拽创建节点

拖拽节点面板node-panel.vue

html 复制代码
<template>
  <div class="node-panel">
    <div
      v-for="(item, key) in state.nodePanel"
      :key="key"
      class="approve-node"
      @mousedown="dragNode(item)"
    >
      <div class="node-shape" :class="'node-' + item.type"></div>
      <div class="node-label">{{ item.text }}</div>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { ILogicFlowNodePanelItem } from "@/types/logic-flow";
import LogicFlow from "@logicflow/core";
import { reactive } from "vue";
const props = defineProps<{ lf?: LogicFlow }>();
const state = reactive({
  nodePanel: [
    {
      type: "approver",
      text: "用户活动",
    },
    {
      type: "link",
      text: "连接点",
    },
    {
      type: "review",
      text: "传阅",
    },
  ],
});
const dragNode = (item: ILogicFlowNodePanelItem) => {
  props.lf?.dnd.startDrag({
    type: item.type,
    text: item.text,
  });
};
</script>

自定义节点/边index.ts

ts 复制代码
/**
 * @description 注册节点
 * @export
 * @param {LogicFlow} lf
 * @return {*}
 */
export function registeNode(lf: ShallowRef<LogicFlow | undefined>) {
  /**
   * @description 自定义开始节点
   */
  class StartNode extends CircleNode {
    getShape() {
      const { x, y } = this.props.model;
      const style = this.props.model.getNodeStyle();
      return h("g", {}, [
        h("circle", {
          ...style,
          cx: x,
          cy: y,
          r: 30,
          stroke: "#000",
          fill: "#000",
        }),
      ]);
    }
    getText() {
      const { x, y, text } = this.props.model;
      return h(
        "text",
        {
          x: x,
          y: y,
          fill: "#fff",
          textAnchor: "middle",
          alignmentBaseline: "middle",
          style: { fontSize: 12 },
        },
        text.value
      );
    }
  }
  class StartNodeModel extends CircleNodeModel {
    setAttributes() {
      this.r = 30;
      this.isSelected = false;
    }
    getConnectedTargetRules() {
      const rules = super.getConnectedTargetRules();
      const geteWayOnlyAsTarget = {
        message: "开始节点只能连出,不能连入!",
        validate: (source?: BaseNodeModel, target?: BaseNodeModel) => {
          let isValid = true;
          if (target) {
            isValid = false;
          }
          return isValid;
        },
      };
      rules.push(geteWayOnlyAsTarget);
      return rules;
    }
    getConnectedSourceRules() {
      const rules = super.getConnectedSourceRules();
      const onlyOneOutEdge = {
        message: "开始节点只能连出一条线!",
        validate: (source?: BaseNodeModel, target?: BaseNodeModel) => {
          let isValid = true;
          if (source?.outgoing.edges.length) {
            isValid = false;
          }
          return isValid;
        },
      };
      rules.push(onlyOneOutEdge);
      return rules;
    }
    createId() {
      return uuidv4();
    }
  }
  lf.value?.register({
    type: "start",
    view: StartNode,
    model: StartNodeModel,
  });
  /**
   * @description 自定义发起节点
   */
  class LaunchNode extends RectNode {
    getShape() {
      const { x, y, width, height, radius } = this.props.model;
      const style = this.props.model.getNodeStyle();
      return h("g", {}, [
        h("rect", {
          ...style,
          x: x - width / 2,
          y: y - height / 2,
          rx: radius,
          ry: radius,
          width: 120,
          height: 50,
          stroke: "#000",
          fill: "#000",
        }),
      ]);
    }
    getText() {
      const { x, y, text, width, height } = this.props.model;
      return h(
        "foreignObject",
        {
          x: x - width / 2,
          y: y - height / 2,
          className: "foreign-object",
          style: {
            width: width,
            height: height,
          },
        },
        [
          h(
            "p",
            {
              style: {
                fontSize: 12,
                width: width,
                height: height,
                lineHeight: height + "px",
                whiteSpace: "nowrap",
                overflow: "hidden",
                textOverflow: "ellipsis",
                textAlign: "center",
                padding: "0 8px",
                boxSizing: "border-box",
                margin: "0",
                color: "#fff",
              },
            },
            text.value
          ),
        ]
      );
    }
  }
  class LaunchModel extends RectNodeModel {
    setAttributes() {
      this.width = 120;
      this.height = 50;
      this.radius = 4;
      this.isSelected = false;
    }
    getConnectedSourceRules() {
      const rules = super.getConnectedSourceRules();
      const notAsTarget = {
        message: "不能连接自己",
        validate: (source?: BaseNodeModel, target?: BaseNodeModel) => {
          let isValid = true;
          if (source?.id === target?.id) {
            isValid = false;
          }
          return isValid;
        },
      };
      rules.push(notAsTarget);
      return rules;
    }
    createId() {
      return uuidv4();
    }
  }
  lf.value?.register({
    type: "launch",
    view: LaunchNode,
    model: LaunchModel,
  });
  /**
   * @description 自定义审批节点
   */
  class ApproverNode extends RectNode {
    getShape() {
      const { x, y, width, height, radius } = this.props.model;
      const style = this.props.model.getNodeStyle();
      return h("g", {}, [
        h("rect", {
          ...style,
          x: x - width / 2,
          y: y - height / 2,
          rx: radius,
          ry: radius,
          width: 120,
          height: 50,
          stroke: "#facd91",
          fill: "#facd91",
        }),
      ]);
    }
    getText() {
      const { x, y, text, width, height } = this.props.model;
      return h(
        "foreignObject",
        {
          x: x - width / 2,
          y: y - height / 2,
          className: "foreign-object",
          style: {
            width: width,
            height: height,
          },
        },
        [
          h(
            "p",
            {
              style: {
                fontSize: 12,
                width: width,
                height: height,
                lineHeight: height + "px",
                whiteSpace: "nowrap",
                overflow: "hidden",
                textOverflow: "ellipsis",
                textAlign: "center",
                padding: "0 8px",
                boxSizing: "border-box",
                margin: "0",
              },
            },
            text.value
          ),
        ]
      );
    }
  }
  class ApproverModel extends RectNodeModel {
    setAttributes() {
      this.width = 120;
      this.height = 50;
      this.radius = 4;
      this.isSelected = false;
    }
    getConnectedSourceRules() {
      const rules = super.getConnectedSourceRules();
      const notAsTarget = {
        message: "不能连接自己",
        validate: (source?: BaseNodeModel, target?: BaseNodeModel) => {
          let isValid = true;
          if (source?.id === target?.id) {
            isValid = false;
          }
          return isValid;
        },
      };
      rules.push(notAsTarget);
      return rules;
    }
    createId() {
      return uuidv4();
    }
  }
  lf.value?.register({
    type: "approver",
    view: ApproverNode,
    model: ApproverModel,
  });
  /**
   * @description 自定义连接点节点
   */
  class LinkNode extends RectNode {
    getShape() {
      const { x, y, width, height, radius } = this.props.model;
      const style = this.props.model.getNodeStyle();
      return h("g", {}, [
        h("rect", {
          ...style,
          x: x - width / 2,
          y: y - height / 2,
          rx: radius,
          ry: radius,
          width: 120,
          height: 50,
          stroke: "#caf982",
          fill: "#caf982",
        }),
      ]);
    }
    getText() {
      const { x, y, text, width, height } = this.props.model;
      return h(
        "foreignObject",
        {
          x: x - width / 2,
          y: y - height / 2,
          className: "foreign-object",
          style: {
            width: width,
            height: height,
          },
        },
        [
          h(
            "p",
            {
              style: {
                fontSize: 12,
                width: width,
                height: height,
                lineHeight: height + "px",
                whiteSpace: "nowrap",
                overflow: "hidden",
                textOverflow: "ellipsis",
                textAlign: "center",
                padding: "0 8px",
                boxSizing: "border-box",
                margin: "0",
              },
            },
            text.value
          ),
        ]
      );
    }
  }
  class LinkModel extends RectNodeModel {
    setAttributes() {
      this.width = 120;
      this.height = 50;
      this.radius = 4;
      this.isSelected = false;
    }
    getConnectedSourceRules() {
      const rules = super.getConnectedSourceRules();
      const notAsTarget = {
        message: "不能连接自己",
        validate: (source?: BaseNodeModel, target?: BaseNodeModel) => {
          let isValid = true;
          if (source?.id === target?.id) {
            isValid = false;
          }
          return isValid;
        },
      };
      rules.push(notAsTarget);
      return rules;
    }
    createId() {
      return uuidv4();
    }
  }
  lf.value?.register({
    type: "link",
    view: LinkNode,
    model: LinkModel,
  });
  /**
   * @description 自定义传阅节点
   */
  class ReviewNode extends RectNode {
    getShape() {
      const { x, y, width, height, radius } = this.props.model;
      const style = this.props.model.getNodeStyle();
      return h("g", {}, [
        h("rect", {
          ...style,
          x: x - width / 2,
          y: y - height / 2,
          rx: radius,
          ry: radius,
          width: 120,
          height: 50,
          stroke: "#81d3f8",
          fill: "#81d3f8",
        }),
      ]);
    }
    getText() {
      const { x, y, text, width, height } = this.props.model;
      return h(
        "foreignObject",
        {
          x: x - width / 2,
          y: y - height / 2,
          className: "foreign-object",
          style: {
            width: width,
            height: height,
          },
        },
        [
          h(
            "p",
            {
              style: {
                fontSize: 12,
                width: width,
                height: height,
                lineHeight: height + "px",
                whiteSpace: "nowrap",
                overflow: "hidden",
                textOverflow: "ellipsis",
                textAlign: "center",
                padding: "0 8px",
                boxSizing: "border-box",
                margin: "0",
              },
            },
            text.value
          ),
        ]
      );
    }
  }
  class ReviewModel extends RectNodeModel {
    setAttributes() {
      this.width = 120;
      this.height = 50;
      this.radius = 4;
      this.isSelected = false;
    }
    getConnectedSourceRules() {
      const rules = super.getConnectedSourceRules();
      const notAsTarget = {
        message: "不能连接自己",
        validate: (source?: BaseNodeModel, target?: BaseNodeModel) => {
          let isValid = true;
          if (source?.id === target?.id) {
            isValid = false;
          }
          return isValid;
        },
      };
      rules.push(notAsTarget);
      return rules;
    }
    createId() {
      return uuidv4();
    }
  }
  lf.value?.register({
    type: "review",
    view: ReviewNode,
    model: ReviewModel,
  });
  /**
   * @description 结束节点
   */
  class FinishNode extends CircleNode {
    getShape() {
      const { x, y } = this.props.model;
      const style = this.props.model.getNodeStyle();
      return h("g", {}, [
        h("circle", {
          ...style,
          cx: x,
          cy: y,
          r: 30,
          stroke: "#000",
          fill: "#000",
        }),
      ]);
    }
    getText() {
      const { x, y, text } = this.props.model;
      return h(
        "text",
        {
          x: x,
          y: y,
          fill: "#fff",
          textAnchor: "middle",
          alignmentBaseline: "middle",
          style: { fontSize: 12 },
        },
        text.value
      );
    }
  }
  class FinishModel extends CircleNodeModel {
    setAttributes() {
      this.r = 30;
      this.isSelected = false;
    }
    getConnectedSourceRules() {
      const rules = super.getConnectedSourceRules();
      const notAsTarget = {
        message: "终止节点不能作为连线的起点",
        validate: () => false,
      };
      rules.push(notAsTarget);
      return rules;
    }
    createId() {
      return uuidv4();
    }
  }
  lf.value?.register({
    type: "end",
    view: FinishNode,
    model: FinishModel,
  });
  /**
   * @description 虚线
   */
  class DashedLineModel extends PolylineEdgeModel {
    getEdgeStyle() {
      const style = super.getEdgeStyle();
      style.stroke = "#000";
      style.strokeDasharray = "3 3";
      return style;
    }
  }
  lf.value?.register({
    type: "dashedLine",
    view: PolylineEdge,
    model: DashedLineModel,
  });
  /**
   * @description 开始的连线
   */
  class StartPolylineModel extends PolylineEdgeModel {
    setAttributes() {
      this.isSelected = false;
      this.isHitable = false;
    }
  }
  lf.value?.register({
    type: "startPolyline",
    view: PolylineEdge,
    model: StartPolylineModel,
  });
}

注册logicflow并使用自定义节点

html 复制代码
<template>
  <div class="logic-flow-container">
    <div class="logic-flow-header">
      <el-button type="primary" @click="getData">获取数据</el-button>
      <el-button type="primary" @click="submit">提交</el-button>
    </div>
    <div class="logic-flow-main">
      <div class="logic-flow" ref="logicFlowRef"></div>
      <Setting
        class="logic-flow-setting"
        :data="nodeData!"
        :lf="lf"
        :type="state.settingType"
      ></Setting>
      <NodePanel :lf="lf"></NodePanel>
    </div>
    <!-- 当lf有值 才能注册事件 -->
    <Control v-if="lf" :lf="lf"></Control>
  </div>
</template>

<script lang="ts">
export default { name: "LogicFlow" };
</script>
<script lang="ts" setup>
import LogicFlow from "@logicflow/core";
import "@logicflow/core/lib/style/index.css";
import "@logicflow/extension/lib/style/index.css";
import { onMounted, reactive, ref, ShallowRef, shallowRef } from "vue";
import NodePanel from "./components/node-panel.vue";
import { registeNode, registerKeyboard, requiredConfig } from "./index";
import { ElMessage } from "element-plus";
import Control from "./components/control.vue";
import Setting from "./components/setting.vue";
import { SettingType } from "@/types/logic-flow";

const logicFlowRef = ref<HTMLDivElement>();
const nodeData = ref<LogicFlow.NodeData | LogicFlow.EdgeData>(); // 节点数据
const state = reactive({
  settingType: "all" as SettingType,
});
const lf = shallowRef<LogicFlow>();

const getSettingInfo = (data: LogicFlow.NodeData | LogicFlow.EdgeData) => {
  switch (data.type) {
    case "launch":
      nodeData.value = data;
      state.settingType = data.type;
      break;
    case "approver":
      nodeData.value = data;
      state.settingType = data.type;
      break;
    case "link":
      nodeData.value = data;
      state.settingType = data.type;
      break;
    case "review":
      nodeData.value = data;
      state.settingType = data.type;
      break;
    case "polyline":
    case "dashedLine":
      nodeData.value = data;
      state.settingType = data.type;
      break;
  }
};
/**
 * @description 注册事件
 */
const initEvent = (lf: ShallowRef<LogicFlow | undefined>) => {
  lf.value?.on("blank:click", (e) => {
    state.settingType = "all";
  });
  lf.value?.on("node:mousedown", ({ data }) => {
    lf.value?.selectElementById(data.id, false);
    getSettingInfo(data);
  });
  lf.value?.on("edge:click", ({ data }) => {
    lf.value?.selectElementById(data.id, false);
    getSettingInfo(data);
  });
  lf.value?.on("connection:not-allowed", (data) => {
    ElMessage.error(data.msg);
    return false;
  });
  lf.value?.on("node:dnd-add", ({ data }) => {
    // 选中节点 更改信息
    lf.value?.selectElementById(data.id, false);
    getSettingInfo(data);
    lf.value?.container.focus(); // 聚焦 能够使用键盘操作
  });
};
/**
 * @description 获取数据
 */
const getData = () => {
  console.log(lf.value?.getGraphData());
};
/**
 * @description 提交 验证数据
 */
const submit = () => {
  const { nodes } = lf.value?.getGraphData() as LogicFlow.GraphData;
  for (let index = 0; index < nodes.length; index++) {
    const data = nodes[index];
    const { properties } = data;
    // 循环配置项
    for (const key in properties) {
      // 数组配置项 判断是否为空
      if (Array.isArray(properties[key])) {
        if (requiredConfig[key] && properties[key].length === 0) {
          return ElMessage.error(
            `${data.text?.value}节点 ${requiredConfig[key]}`
          );
        }
      } else {
        // 非数组配置项 判断是否为空
        if (requiredConfig[key] && !properties[key]) {
          return ElMessage.error(
            `${data.text?.value}节点 ${requiredConfig[key]}`
          );
        }
      }
    }
  }
  console.log(lf.value?.getGraphData());
};

onMounted(() => {
  lf.value = new LogicFlow({
    container: logicFlowRef.value!,
    grid: true,
    keyboard: {
      enabled: true,
      shortcuts: registerKeyboard(lf, nodeData),
    },
    textEdit: false,
  });
  registeNode(lf);
  initEvent(lf);
  lf.value.render({
    nodes: [
      {
        id: "node_1",
        type: "start",
        x: 100,
        y: 300,
        properties: {
          width: 60,
          height: 60,
        },
        text: {
          x: 100,
          y: 300,
          value: "开始",
        },
      },
      {
        id: "node_2",
        type: "launch",
        x: 100,
        y: 400,
        properties: {
          width: 120,
          height: 50,
        },
        text: {
          x: 100,
          y: 400,
          value: "发起流程",
        },
      },
      {
        id: "node_3",
        type: "end",
        x: 100,
        y: 600,
        properties: {
          width: 60,
          height: 60,
        },
        text: {
          x: 100,
          y: 600,
          value: "结束",
        },
      },
    ],
    edges: [
      {
        id: "edge_1",
        type: "startPolyline",
        sourceNodeId: "node_1",
        targetNodeId: "node_2",
      },
      {
        id: "edge_2",
        type: "polyline",
        sourceNodeId: "node_2",
        targetNodeId: "node_3",
      },
    ],
  });
  lf.value.translateCenter(); // 将图形移动到画布中央
});
</script>

右侧的配置设置

  1. 通过componentIs实现不同的配置组件
  2. 通过logicflow的setProperties()函数,将配置项注入节点/边的properties对象中,目的是传参和回显的时候方便



人员选择组件

正选、反选、回显,可作为一个单独组件使用,目前使用的是el-tree,数据量大时可考虑虚拟树

html 复制代码
<template>
  <MyDialog
    v-model="state.visible"
    title="选择人员"
    width="800px"
    @close="close"
    @cancel="close"
    @submit="submit"
  >
    <div class="type">
      <label>
        <span>发起人:</span>
        <el-radio-group v-model="state.type">
          <el-radio value="1">指定人员</el-radio>
          <el-radio value="2">角色</el-radio>
        </el-radio-group>
      </label>
    </div>
    <div class="panel">
      <div class="left-panel">
        <div class="panel-title">人员选择</div>
        <div class="search">
          <el-input
            v-model="state.filterText"
            style="width: 100%"
            placeholder="请输入筛选内容"
          />
        </div>
        <div class="content">
          <el-tree
            ref="treeRef"
            :data="state.data"
            show-checkbox
            node-key="key"
            :check-on-click-node="true"
            :filter-node-method="filterNode"
            @check-change="checkChange"
          />
        </div>
      </div>
      <div class="right-panel">
        <div class="panel-title">已选择</div>
        <div class="content checked-content">
          <el-tag
            v-for="tag in state.checkedList"
            :key="tag.key"
            closable
            type="primary"
            @close="handleClose(tag.key)"
          >
            {{ tag.label }}
          </el-tag>
        </div>
      </div>
    </div>
  </MyDialog>
</template>

<script lang="ts">
export default { name: "ChoosePerson" };
</script>
<script lang="ts" setup>
import { ElTree } from "element-plus";
import { nextTick, reactive, ref, watch } from "vue";
interface Tree {
  [key: string]: any;
}
const state = reactive({
  visible: false,
  type: "1",
  filterText: "",
  value: [],
  data: [
    {
      label: "张三",
      key: "1",
    },
    {
      label: "李四",
      key: "2",
    },
    {
      label: "王五",
      key: "3",
      children: [
        {
          label: "王五1",
          key: "31",
        },
        {
          label: "王五2",
          key: "32",
        },
      ],
    },
  ],
  checked: [] as string[],
  checkedList: [] as { label: string; key: string }[],
});
const treeRef = ref<InstanceType<typeof ElTree>>();
const emits = defineEmits(["submit"]);
/**
 * @description 筛选节点
 */
watch(
  () => state.filterText,
  (val) => {
    treeRef.value!.filter(val);
  }
);
const open = (checked: string[]) => {
  state.visible = true;
  nextTick(() => {
    state.checked = checked;
    treeRef.value?.setCheckedKeys([...checked], false);
  });
};
const close = () => {
  state.visible = false;
  state.filterText = "";
};
const submit = () => {
  emits("submit", state.checked, state.checkedList);
  close();
};
/**
 * @description 筛选节点
 */
const filterNode = (value: string, data: Tree) => {
  if (!value) return true;
  return data.label.includes(value);
};
/**
 * @description 选中节点
 */
const checkChange = () => {
  // 已选的id string[] 用来提交
  state.checked = treeRef.value
    ?.getCheckedNodes(true, false)
    .map((item) => item.key) as string[];
  // 已选的对象 {label: string; key: string}[] 用来展示tag
  state.checkedList = treeRef.value
    ?.getCheckedNodes(true, false)
    .map((item) => {
      return {
        label: item.label,
        key: item.key,
      };
    })!;
};
/**
 * @description 删除已选人员
 */
const handleClose = (key: string) => {
  state.checkedList = state.checkedList.filter((item) => item.key !== key);
  treeRef.value?.setCheckedKeys(
    state.checkedList.map((item) => item.key),
    false
  );
};
/**
 * @description 清空已选人员
 */
const clear = () => {
  state.checkedList = [];
  state.checked = [];
  treeRef.value?.setCheckedKeys([], false);
};
defineExpose({
  open,
  clear,
});
</script>

<style lang="scss" scoped>
.type {
  display: flex;
  align-items: center;
  margin-bottom: 20px;
  span {
    margin-right: 10px;
  }
  label {
    display: flex;
    align-items: center;
  }
}
.panel {
  width: 100%;
  display: flex;
  .left-panel {
    flex: 1;
    border: 1px solid #ccc;
    border-radius: 4px;
    .search {
      padding: 6px 10px;
    }
  }
  .right-panel {
    flex: 1;
    margin-left: 20px;
    border: 1px solid #ccc;
    border-radius: 4px;
  }
  .panel-title {
    padding: 10px 0;
    font-size: 14px;
    font-weight: bold;
    background-color: #f5f5f5;
    text-align: center;
  }
  .content {
    max-height: 400px;
    min-height: 200px;
    overflow: auto;
  }
  .checked-content {
    padding: 6px 10px;
    .el-tag + .el-tag {
      margin-left: 10px;
    }
  }
}
</style>

自定义快捷键,根据源码改编

ts 复制代码
/**
 * @description 注册键盘事件
 * @export
 * @param {(ShallowRef<LogicFlow | undefined>)} lf
 * @param {(Ref<LogicFlow.NodeData | LogicFlow.EdgeData | undefined>)} nodeData
 * @return {*}
 */
export function registerKeyboard(
  lf: ShallowRef<LogicFlow | undefined>,
  nodeData: Ref<LogicFlow.NodeData | LogicFlow.EdgeData | undefined>
) {
  let copyNodes = undefined as LogicFlow.NodeData[] | undefined;
  let TRANSLATION_DISTANCE = 40;
  let CHILDREN_TRANSLATION_DISTANCE = 40;
  const cv = [
    {
      keys: ["ctrl + c", "cmd + c"],
      callback: () => {
        copyNodes = lf.value?.getSelectElements().nodes;
      },
    },
    {
      keys: ["ctrl + v", "cmd + v"],
      callback: () => {
        const startOrEndNode = copyNodes?.find(
          (node) =>
            node.type === "start" ||
            node.type === "end" ||
            node.type === "launch"
        );
        if (startOrEndNode) {
          return true;
        }
        if (copyNodes) {
          lf.value?.clearSelectElements();
          copyNodes.forEach(function (node) {
            node.x += TRANSLATION_DISTANCE;
            node.y += TRANSLATION_DISTANCE;
            node.text!.x += TRANSLATION_DISTANCE;
            node.text!.y += TRANSLATION_DISTANCE;
            return node;
          });
          let addElements = lf.value?.addElements(
            { nodes: copyNodes, edges: [] },
            CHILDREN_TRANSLATION_DISTANCE
          );
          if (!addElements) return true;
          addElements.nodes.forEach(function (node) {
            nodeData.value = node.getData();
            return lf.value?.selectElementById(node.id, true);
          });
          CHILDREN_TRANSLATION_DISTANCE =
            CHILDREN_TRANSLATION_DISTANCE + TRANSLATION_DISTANCE;
        }
        return false;
      },
    },
    {
      keys: ["backspace"],
      callback: () => {
        const elements = lf.value?.getSelectElements(true);
        if (elements) {
          lf.value?.clearSelectElements();
          elements.edges.forEach(function (edge) {
            return edge.id && lf.value?.deleteEdge(edge.id);
          });
          elements.nodes.forEach(function (node) {
            if (
              node.type === "start" ||
              node.type === "end" ||
              node.type === "launch"
            ) {
              return true;
            }
            return node.id && lf.value?.deleteNode(node.id);
          });
          return false;
        }
      },
    },
  ];
  return cv;
}

仓库地址
在线预览

相关推荐
王哲晓9 分钟前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
理想不理想v13 分钟前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云23 分钟前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
aPurpleBerry1 小时前
JS常用数组方法 reduce filter find forEach
javascript
GIS程序媛—椰子2 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
ZL不懂前端2 小时前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x2 小时前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
我血条子呢2 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落3 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt