App.vue 10.2 KB
<template>
  <div class="app" style="height: 100vh">
    <vue-flow-editor
      ref="editor"
      :data="state.data"
      :grid="showGrid"
      :miniMap="showMiniMap"
      :onRef="onRef"
      :multipleSelect="true"
      :loading="state.editorLoading"
      :beforeDelete="handleBeforeDelete"
      :afterDelete="handleAfterDelete"
      :beforeAdd="handleBeforeAdd"
      :afterAdd="handleAfterAdd"
      @dblclick-node="onDblclickNode"
      @dblclick-edge="onDblClickEdge"
      :activityConfig="state.activityConfig"
    >
      <template v-slot:menu>
        <vue-flow-edit-menu-group label="活动节点" value>
          <vue-flow-edit-menu
            v-for="(value, key) in state.activityConfig"
            :key="key"
            :model="{ activity: key, text: value.text, desc: value.desc }"
          >
            <template v-slot:content>
              <div class="activity-menu">
                <img :src="value.img" />
                <span>{{ value.text }}</span>
              </div>
            </template>
          </vue-flow-edit-menu>
        </vue-flow-edit-menu-group>
        <vue-flow-edit-menu-group
          v-for="(group, groupIndex) in state.menuData"
          :label="group.label"
          :key="groupIndex"
          value
        >
          <vue-flow-edit-menu
            v-for="(menu, menuIndex) in group.menus"
            :key="menuIndex"
            :model="menu"
          />
        </vue-flow-edit-menu-group>
      </template>

      <template v-slot:model>
        <el-form
          v-if="!!state.detailModel"
          ref="form"
          :model="state.detailModel"
          label-width="100px"
        >
          <template v-if="state.detailModel.activity === undefined">
            <el-form-item label="节点名称" prop="label">
              <el-input v-model="state.detailModel.label" />
            </el-form-item>
            <el-form-item label="测试属性" prop="label">
              <el-input v-model="state.detailModel.data.test" />
            </el-form-item>

            <template v-if="state.detailModel.type !== 'edge'">
              <el-form-item label="节点背景色" prop="style.fill">
                <el-color-picker v-model="state.detailModel.style.fill" />
              </el-form-item>
              <el-form-item label="节点边框色" prop="style.stroke">
                <el-color-picker v-model="state.detailModel.style.stroke" />
              </el-form-item>
              <el-form-item label="节点文字色" prop="labelCfg.style.stroke">
                <el-color-picker
                  v-model="state.detailModel.labelCfg.style.fill"
                />
              </el-form-item>
            </template>
          </template>
          <template v-else>
            <el-form-item label="活动标题">
              <el-input v-model="state.detailModel.text" />
            </el-form-item>
            <el-form-item label="活动副标题">
              <el-input v-model="state.detailModel.desc" />
            </el-form-item>
            <el-form-item label="活动类型">
              <el-select v-model="state.detailModel.activity">
                <el-option
                  v-for="(value, key) in state.activityConfig"
                  :key="key"
                  :label="value.text"
                  :value="key"
                />
              </el-select>
            </el-form-item>
          </template>
        </el-form>
      </template>

      <template v-slot:toolbar>
        <el-tooltip content="测试工具栏插槽">
          <div class="vue-flow-editor-toolbar-item" @click="logData">
            <i class="el-icon-search" />
          </div>
        </el-tooltip>
      </template>

      <template v-slot:foot>
        <el-button type="primary" @click="save">保存</el-button>
        <el-button @click="cancel">取消</el-button>
      </template>
    </vue-flow-editor>
  </div>
</template>

<script lang="ts">
import { reactive, onMounted } from "vue";
import { AppData } from "./data.js";
import { staticPath } from "./utils";
import { ElNotification } from 'element-plus'

const G6 = (window as any).G6.default as any;

function delay(time) {
  return new Promise((resolve) => setTimeout(resolve, time));
}

export default {
  setup(props, context) {
    const state = reactive({
      data: AppData,
      detailModel: null,
      editorLoading: false,
      selectOptions: [
        { label: "待确认", value: "0" },
        { label: "填写表单", value: "1" },
        { label: "部门负责人审批", value: "2" },
        { label: "总经理审批", value: "3" },
      ],
      menuData: [
        {
          label: "流程节点",
          menus: [
            { label: "开始", shape: "ellipse", id: "start-node" },
            { label: "结束", shape: "ellipse", id: "end-node" },
            { label: "审批节点", busType: "123" },
            { label: "判断节点", shape: "diamond" },
          ],
        },
        {
          label: "其他形状节点",
          menus: [
            { label: "矩形节点", shape: "rect" },
            { label: "圆形节点", shape: "circle" },
            { label: "椭圆节点", shape: "ellipse" },
            { label: "菱形节点", shape: "diamond" },
            { label: "三角形节点", shape: "triangle" },
            { label: "星形节点", shape: "star" },
          ],
        },
      ],
      activityConfig: {
        advertisement: {
          text: "广告宣传1",
          desc: "通过广告宣传新品",
          color: "#9283ed",
          img: 'https://cdn.ipadbiz.cn/oa/advertisement-node.svg',
        },
        coupon: {
          text: "优惠券",
          desc: "发放奖励优惠券",
          color: "#ed8383",
          img: 'https://cdn.ipadbiz.cn/oa/coupon-node.svg',
        },
        crowd: {
          text: "用户反馈",
          desc: "收集用户反馈信息",
          color: "#92dba8",
          img: 'https://cdn.ipadbiz.cn/oa/crowd-node.svg',
        },
      },
    });

    onMounted(() => {
      // 显示提示框的标志位
      var showConfirmation = true;

      // 监听 beforeunload 事件
      window.addEventListener('beforeunload', function (event) {
        if (showConfirmation) {
          // 取消事件的默认行为(弹出确认对话框)
          event.preventDefault();
          // Chrome 和 Firefox 需要返回一个值以显示确认对话框
          event.returnValue = '';

          // 显示自定义的提示信息
          var confirmationMessage = '确定要离开此页面吗?';
          (event || window.event).returnValue = confirmationMessage;  // 兼容旧版浏览器

          return confirmationMessage;
        }
      });

      // 监听 unload 事件
      window.addEventListener('unload', function () {
        // 设置标志位为 false,避免在刷新页面时再次显示提示框
        showConfirmation = false;
      });
    });

    let editor;

    function onDblclickNode(e) {
      const model = G6.Util.clone(e.item.get("model"));
      model.style = model.style || {};
      model.labelCfg = model.labelCfg || { style: {} };

      state.detailModel = model;
      editor.openModel();
    }

    function onDblClickEdge(e) {
      const { source, target, style, labelCfg, label } = e.item.get("model");
      const model = {
        label,
        source,
        target,
        style: style || {},
        labelCfg: labelCfg || { style: {} },
        type: null,
        id: null,
      };
      model.type = e.item.get("type");
      model.id = e.item.get("id");

      state.detailModel = model;
      editor.openModel();
    }

    function cancel() {
      editor.closeModel();
    }

    function save() {
      editor.updateModel(state.detailModel);
      editor.closeModel();
    }

    async function handleBeforeDelete(model, type) {
      if (type === "node") {
        if (model.label === "开始") {
          state.editorLoading = true;
          await delay(1000);
          state.editorLoading = false;
          ElNotification.error("不可以删除【开始】节点");
          return Promise.reject("reject");
        }
      }
    }

    function handleAfterDelete(model, type) {
      if (type === "edge") {
        console.log("delete edge");
      } else {
        console.log("after delete", model.label, { ...model });
      }
    }

    function handleBeforeAdd(model, type) {
      if (type === "edge") {
        if (model.source === "end-node") {
          ElNotification.error("结束节点不能输出连线其他节点");
          return Promise.reject("reject");
        }
      }
      if (type === "node") {
        if (model.id === "start-node" || model.id === "end-node") {
          const data = editor.editorState.graph.save();
          for (let i = 0; i < data.nodes.length; i++) {
            const node = data.nodes[i];
            if (node.id === model.id) {
              ElNotification.error(
              `只能有一个${model.id === "start-node" ? "开始" : "结束"}节点`
              );
              return Promise.reject("reject");
            }
          }
        }
      }
    }

    function handleAfterAdd(model, type) {
      if (type === "edge") {
        console.log(`新增连接线`);
      }
    }

    function logData() {
      let { nodes, edges } = editor.editorState.graph.save();
      nodes = nodes.map(
        ({ data, id, label, shape, x, y, text, desc, img }) => ({
          data,
          id,
          label,
          shape,
          x,
          y,
          text,
          desc,
          img,
        })
      );
      edges = edges.map(({ source, sourceAnchor, target, targetAnchor }) => ({
        source,
        sourceAnchor,
        target,
        targetAnchor,
      }));
      console.log(JSON.stringify({ nodes, edges }, null, 2));
    }

    return {
      state,

      showGrid: true,
      showMiniMap: true,

      onDblclickNode,
      onDblClickEdge,
      cancel,
      save,
      handleBeforeDelete,
      handleAfterDelete,
      handleBeforeAdd,
      handleAfterAdd,

      logData,

      onRef: (e) => (editor = e),
      staticPath,
    };
  },
};
</script>

<style lang="scss">
html,
body {
  padding: 0;
  margin: 0;

  .activity-menu {
    display: flex;
    align-items: center;

    img {
      margin-right: 1em;
      width: 30px;
      height: 30px;
    }
  }
}
</style>