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

news2024/11/26 12:04:54

实现结果

在这里插入图片描述
实现功能:

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

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

拖拽节点面板node-panel.vue

<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

/**
 * @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并使用自定义节点

<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,数据量大时可考虑虚拟树
在这里插入图片描述

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

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

/**
 * @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;
}

仓库地址
在线预览

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2239655.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机器学习——贝叶斯

&#x1f33a;历史文章列表&#x1f33a; 机器学习——损失函数、代价函数、KL散度机器学习——特征工程、正则化、强化学习机器学习——常见算法汇总机器学习——感知机、MLP、SVM机器学习——KNN机器学习——贝叶斯机器学习——决策树机器学习——随机森林、Bagging、Boostin…

大模型书籍推荐丨这本书必看:大语言模型 基础与前沿(附PDF)

哈喽大家好&#xff01;很久都没有更新大模型这块的书了&#xff0c;今天给大家说一下这本&#xff1a;《大语言模型&#xff1a;基础与前沿》&#xff0c;本书深入阐述了大语言模型的基本概念和算法、研究前沿以及应用&#xff0c;涵盖大语言模型的广泛主题&#xff0c;从基础…

Tofu AI视频处理模块视频输入配置方法

应用Tofu产品对网络视频进行获取做视频处理时&#xff0c;首先需要配置Tofu产品的硬件连接关系与设备IP地址、视频拉流地址。 步骤1 Tofu设备点对点直连或者通过交换机连接到电脑&#xff0c;电脑IP配置到与Tofu默认IP地址同一个网段。 打开软件 点击右上角系统设置 单击左侧…

MTSET可溶于DMSO、DMF、THF等有机溶剂,并在水中有轻微的溶解性,91774-25-3

一、基本信息 中文名称&#xff1a;[2-(三甲基铵)乙基]甲硫基磺酸溴&#xff1b;MTSET巯基反应染料 英文名称&#xff1a;MTSET&#xff1b;[2-(Trimethylammonium)ethyl]methanethiosulfonate Bromide CAS号&#xff1a;91774-25-3 分子式&#xff1a;C6H16BrNO2S2 分子量…

BERT框架详解

BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;是由Google在2018年提出的一种自然语言处理&#xff08;NLP&#xff09;模型。BERT通过使用Transformer架构&#xff0c;实现了对文本的双向上下文理解&#xff0c;从而在多个NLP任务中取…

定时器(QTimer)与随机数生成器(QRandomGenerator)的应用实践——Qt(C++)

一、QTimer与QRandomGenerator &#xff08;一&#xff09;QTimer&#xff08;定时器&#xff09;[2] QTimer类为定时功能提供了一个高级编程接口。在使用QTimer时&#xff0c;实例化一个QTimer对象并将其timeout()发射信号与合适的信号槽相连接。通过调用QTimer的start()函数…

Linux 进程线程间通信总结

线程 线程共享存储空间主要带来的问题是数据同步和互斥。由于线程在同一进程中运行&#xff0c;它们共享相同的内存空间&#xff0c;任何线程都可以访问共享数据。这样&#xff0c;多个线程并发执行时&#xff0c;可能会导致以下两种主要问题&#xff1a; 互斥问题&#xff0…

什么是RAG? LangChain的RAG实践!

1. 什么是RAG RAG的概念最先在2020年由Facebook的研究人员在论文《Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks》中提出来。在这篇论文中他们提出了两种记忆类型&#xff1a; 基于预训练模型&#xff08;当时LLM的概念不像现在这么如日中天&#xff0…

Python 进阶函数教程

Python 进阶函数教程 引言 在 Python 编程中&#xff0c;函数是组织代码、提高可重用性和可读性的关键组成部分。尽管许多初学者掌握了基本的函数定义和调用&#xff0c;但 Python 还提供了许多高级功能&#xff0c;使函数更加灵活和强大。本文将深入探讨 Python 中的高级函数…

ReactPress:深入解析技术方案设计与源码

ReactPress Github项目地址&#xff1a;https://github.com/fecommunity/reactpress 欢迎提出宝贵的建议&#xff0c;欢迎一起共建&#xff0c;感谢Star。 ReactPress是一个基于React框架开发的开源发布平台&#xff0c;它不仅仅是一个简单的博客系统&#xff0c;更是一个功能全…

c++实现B树(下)

书接上回小吉讲的是B树的搭建和新增方法的实现&#xff08;blog传送门&#x1f6aa;&#xff1a;B树实现上&#xff09;&#xff08;如果有小可爱对B树还不是很了解的话&#xff0c;可以先看完上一篇blog&#xff0c;再来看小吉的这篇blog&#xff09;。那这一篇主要讲的是B树中…

【漏洞分析】Fastjson最新版本RCE漏洞

01漏洞编号 CVE-2022-25845CNVD-2022-40233CNNVD-202206-1037二、Fastjson知多少 万恶之源AutoType Fastjson的主要功能是将Java Bean序列化为JSON字符串&#xff0c;这样得到的字符串就可以通过数据库等方式进行持久化了。 但是&#xff0c;Fastjson在序列化及反序列化的过…

PSRAM,Flash,SRAM,ROM有什么区别

PSRAM、Flash、SRAM 和 ROM 是四种不同类型的存储器&#xff0c;它们在计算机和嵌入式系统中的用途、特性和工作方式各不相同。下面是这四种存储器的区别和各自的特点&#xff1a; ### 1. **SRAM&#xff08;静态随机存取存储器&#xff09;** - **特性**&#xff1a; - **易…

大数据学习13之Scala基础语法(重点)

1. 简介 Scala 是 Scalable Language 的简写&#xff0c;是一门多范式的编程语言。创始人为 Martin Odersky 马丁奥德斯基。 Scala 这个名字来源于 Scalable Language(可伸缩的语言&#xff09;&#xff0c;它是一门基于 JVM 的多范式编程语言&#xff0c;通俗的说&#xff1a;…

django入门【05】模型介绍——字段选项(二)

文章目录 1、null 和 blank示例说明⭐ null 和 blank 结合使用的几种情况总结&#xff1a; 2、choices**choices 在 Django 中有以下几种形式&#xff1a;**&#xff08;1&#xff09; **简单的列表或元组形式**&#xff08;2&#xff09; **字典映射形式**&#xff08;3&#…

微信小程序:vant组件库安装步骤

前言&#xff1a;在微信小程序中引用vant组件报错&#xff0c;提示路径不存在&#xff0c;这很有可能是因为没有安装构建vant组件库导致。下面是我整理的安装vant组件库的步骤: 第一步&#xff1a;安装node.js(执行完第一步请重启小程序) 具体步骤请看链接&#xff1a;node.js…

Python如何根据给定模型计算权值

目录 一、特征权重的重要性 二、线性回归中的特征权重计算 1. 导入必要的库 2. 创建示例数据集 3. 分割数据集 4. 训练线性回归模型并计算权重 三、特征选择方法 四、实际案例&#xff1a;金融科技数据集 五、总结 在机器学习中&#xff0c;特征权重的计算是理解模型如…

过去几年电子学习的趋势

近年来&#xff0c;在技术和不断变化的学习者期望的推动下&#xff0c;电子学习已经发展成为一种适应性强、沉浸式和社会化的教育形式。个性化已成为最具影响力的趋势之一&#xff0c;Coursera和LinkedIn Learning等平台为个人量身定制内容。这些平台使用人工智能来建议课程、跟…

面相小白的php反序列化漏洞原理剖析

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文整理反序列化漏洞的一些成因原理 建议学习反序列化之前 先对php基础语法与面向对象有个大体的了解 (我觉得我整理的比较细致&#xff0c;了解这俩是个啥就行) 漏洞实战情况 这个漏洞黑盒几乎不会被发现&am…

Flutter中的Material Theme完全指南:从入门到实战

Flutter作为一款热门的跨平台开发框架&#xff0c;其UI组件库Material Design深受开发者喜爱。本文将深入探讨Flutter Material Theme的使用&#xff0c;包括如何借助Material Theme Builder创建符合产品需求的主题风格。通过多个场景和代码实例&#xff0c;让你轻松掌握这一工…