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;
}

仓库地址
在线预览

相关推荐
高山我梦口香糖1 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
black^sugar2 小时前
纯前端实现更新检测
开发语言·前端·javascript
2401_857600953 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js
2401_857600953 小时前
数字时代的医疗挂号变革:SSM+Vue 系统设计与实现之道
前端·javascript·vue.js
GDAL3 小时前
vue入门教程:组件透传 Attributes
前端·javascript·vue.js
小白学大数据3 小时前
如何使用Selenium处理JavaScript动态加载的内容?
大数据·javascript·爬虫·selenium·测试工具
轻口味3 小时前
Vue.js 核心概念:模板、指令、数据绑定
vue.js
2402_857583493 小时前
基于 SSM 框架的 Vue 电脑测评系统:照亮电脑品质之路
前端·javascript·vue.js
java_heartLake4 小时前
Vue3之性能优化
javascript·vue.js·性能优化