流程 设计

实习做这个功能的时候没头脑思路,现在学会了就记录一下完成的过程。下面是效果图
在这里插入图片描述
整体采用链表的数据结构搭建,每一个节点就是如图所示的内容,每一个节点包含自身的描述信息,包含子项child的信息,整体渲染方式采用递归子组件完成。

默认情况下就存在两个视图节点,即开始和节数节点,对应的数据结构如下。后期查找父子级关系的时候采用id和pid去维护。
在这里插入图片描述

const process = ref<FlowNode>({
  id: "root",
  pid: undefined,
  type: "start",
  name: "发起人",
  executionListeners: [],
  formProperties: [],
  child: {
    id: "end",
    pid: "root",
    type: "end",
    name: "流程结束",
    executionListeners: [],
    child: undefined,
  } as EndNode,
} as StartNode);

递归子组件

如下是外部调用子组件的地方,会将初始节点和新增节点和删除节点的操作传入。

<TreeNode :node="props.process" @add-node="addNode" @del-node="delNode" />

在TreeNode组件内部,会根据component动态渲染组件。即:is="nodes[node.type]属性,同时将当前节点的属性传入:node="node" v-bind="$attrs"。然后在该组件的结束位置处调用自身。当没有node.child的时候,递归渲染结束

  <slot />
  <!-- $attrs是收集没有被props接受的属性 -->
  <!-- $attrs中包含了所有未被主动接受的属性和自定义事件 -->
  <component :is="nodes[node.type]" :node="node" v-bind="$attrs">
    <!-- 使用具名插槽 -->
    <template v-for="(value, name) in $slots" #[name]="scope">
      <slot :name="name" v-bind="scope || {}" />
    </template>
  </component>
  <!-- 递归子组件生成一个一个节点 -->
  <TreeNode v-if="node.child" :node="node.child" v-bind="$attrs" />

在这里插入图片描述
以审批人节点为例,内部会将动态组件传递的数据接收,然后封装统一的根节点组件渲染即Node。每一个类似审批人节点的这种样式都是固定的,因此传入相关的图标,颜色等

  <Node
    v-bind="$attrs"
    :node="node"
    icon="el:Stamp"
    color="linear-gradient(89.96deg, #FA6F32 .05%, #FB9337 79.83%)"
  >
    <el-text>{{ content }}</el-text>
  </Node>

主要的渲染和样式布局都在Node节点中完成,布局如下

  <div class="node-box">
    <el-card
      @click="activeNode"
      :class="['node', { 'error-node': errorInfo?.length && !_readOnly }]"
    >
      <template #header>
        <!-- 头部 -->
        <div class="head">
          <div @click.stop v-if="showInput">
            <el-input
              ref="inputRef"
              v-click-outside="onClickOutsize"
              @blur="onClickOutsize"
              maxlength="30"
              v-model="node.name"
            ></el-input>
          </div>
          <el-text v-else @click.stop="onShowInput" tag="b" truncated>
            {{ node.name }}
            <el-icon><EditPen /></el-icon>
          </el-text>
          <slot name="icon">
            <svg-icon :size="30" color="node-icon" v-if="icon" :name="icon" />
          </slot>
        </div>
        <!--删除按钮-->
        <span @click.stop>
          <el-popconfirm
            title="您确定要删除该节点吗?"
            width="200"
            :hide-after="0"
            placement="right-start"
            @confirm="delNode"
          >
            <template #reference>
              <el-button
                class="node-close"
                v-show="close && !_readOnly"
                plain
                circle
                icon="CircleClose"
                size="small"
                type="danger"
              />
            </template>
          </el-popconfirm>
        </span>
        <!--错误提示-->
        <el-tooltip placement="top-start">
          <template #content>
            <div v-for="err in errorInfo" :key="err.id">
              {{ err.message }}
            </div>
          </template>
          <el-icon
            class="warn-icon"
            :size="20"
            v-show="errorInfo?.length && !_readOnly"
          >
            <WarnTriangleFilled @click.stop />
          </el-icon>
        </el-tooltip>
      </template>
      <!-- 插槽 -->
      <slot />
    </el-card>
    <!-- 添加下一个节点 -->
    <Add @add-node="addNode" />
  </div>
const _inject = inject<{
  readOnly?: Ref<boolean>;
  nodesError: Ref<Recordable<ErrorInfo[]>>;
}>("flowDesign", {
  readOnly: ref(false),
  nodesError: ref({}),
});

//透传
const $emits = defineEmits<{
  (e: "addNode", type: NodeType, node: FlowNode): void;
  (e: "delNode", node: FlowNode): void;
  (e: "activeNode", node: FlowNode): void;
}>();

const $props = withDefaults(
  defineProps<{
    icon?: string;
    node: FlowNode;
    color?: string;
    readOnly?: boolean;
    close?: boolean;
  }>(),
  {
    readOnly: false,
    close: true,
  }
);

const errorInfo = computed<ErrorInfo[] | undefined>(() => {
  return _inject.nodesError.value[$props.node.id];
});
const _readOnly = computed(() => _inject.readOnly?.value || $props.readOnly);
const showInput = ref(false);
const inputRef = ref<InputInstance>();

const onShowInput = () => {
  //只读模式的时候不处理
  if (_readOnly.value) return;
  showInput.value = true;
  nextTick(() => {
    inputRef.value?.focus();
  });
};
const onClickOutsize = () => {
  if (showInput.value) showInput.value = false;
};

//点击的时候激活当前节点
const activeNode = () => {
  if (_readOnly.value) return;
  $emits("activeNode", $props.node);
};

//添加节点
const addNode = (type: NodeType) => {
  //$props.node是当前节点
  $emits("addNode", type, $props.node);
};

//删除节点
const delNode = () => {
  $emits("delNode", $props.node);
};

每一个Node节点包含自身节点的数据和添加节点的操作,两者组成一个整体。每一个卡片的头部区域通过props传递实现节点类型展示。主体内容部分则采用插槽。

Node组件中主要传递两个如下方法给Add组件使用

//添加节点
const addNode = (type: NodeType) => {
  //$props.node是当前节点
  $emits("addNode", type, $props.node);
};

//删除节点
const delNode = () => {
  $emits("delNode", $props.node);
};

在这里插入图片描述
在这里插入图片描述
Add组件代码如下

  <div class="add-but">
    <el-popover
      placement="bottom-start"
      ref="popoverRef"
      trigger="click"
      title="添加节点"
      :width="340"
    >
      <el-space wrap>
        <div class="node-select" @click="addApprovalNode">
          <svg-icon name="el:Stamp" />
          <el-text>审批人</el-text>
        </div>
        <div class="node-select" @click="addCcNode">
          <svg-icon name="el:Promotion" />
          <el-text>抄送人</el-text>
        </div>
        <div class="node-select" @click="addExclusiveNode">
          <svg-icon name="el:Share" />
          <el-text>互斥分支</el-text>
        </div>
        <div class="node-select" @click="addTimerNode">
          <svg-icon name="el:Timer" />
          <el-text>计时等待</el-text>
        </div>
        <div class="node-select" @click="addNotifyNode">
          <svg-icon name="el:BellFilled" />
          <el-text>消息通知</el-text>
        </div>
      </el-space>
      <template #reference>
        <el-button
          v-show="!readOnly"
          icon="Plus"
          type="primary"
          style="z-index: 1"
          circle
        ></el-button>
      </template>
    </el-popover>
  </div>

当点击添加的时候会打开如下面板,每一个图标点击的时候都会记录当前要创建新节点的类型信息。

const addApprovalNode = () => {
  $emits("addNode", "approval");
  popoverRef.value?.hide();
};

此时会进入父级Node的 $emits("addNode", type, $props.node);执行语句中,将该步骤层层往上传递给TreeNode的addNode方法处理。
在这里插入图片描述

插入节点

插入一个新节点的时候,用户会触发新增按钮,新增按钮会记录当前用户插入的节点类型以及当前节点(会被作为父节点使用)。
在这里插入图片描述
如下代码是将传入点击的node节点收集,创建一个用户指定的节点结构,然后以链表的方式进行链接维护关系。

const addApproval = (node: FlowNode) => {
  const child = node.child;
  const id = nextId();
  node.child = {
    id: id,
    pid: node.id,
    type: "approval",
    name: "审批人",
    executionListeners: [],
    child: child,
    // 属性
    assigneeType: "user",
    formUser: "",
    formRole: "",
    users: [],
    roles: [],
    leader: 1,
    orgLeader: 1,
    choice: false,
    self: false,
    multi: "sequential",
    multiPercent: 100,
    nobody: "pass",
    nobodyUsers: [],
    formProperties: [],
    operations: {
      complete: true,
      refuse: true,
      back: true,
      transfer: true,
      delegate: true,
      addMulti: false,
      minusMulti: false,
    },
  } as ApprovalNode;
  if (child) {
    child.pid = id;
  }
};

emit触发后会执行这里的addNode操作,根据不同的type类型执行不同的创建函数,同时传入父节点node。

const addNode = (type: NodeType, node: FlowNode) => {
  //node当前插入父节点
  //映射函数添加
  const addMap: Recordable<(node: FlowNode) => void> = {
    exclusive: addExclusive,
    condition: addCondition,
    cc: addCc,
    timer: addTimer,
    notify: addNotify,
    approval: addApproval,
  };
  const fun = addMap[type];
  fun && fun(node);
};

删除节点

删除节点再每一个Node节点中去触发delNode函数将当前删除的节点信息传递给父组件完成。删除一个节点,需要找到当前删除节点的父节点,父节点和删除节点的子项关系即可。

删除的时候需要将整个链表和删除节点的数据传入。next.id === del.pid判断删除节点的父节点id和当前节点id是否一致,一直则代表找到删除节点的父节点信息。否则继续递归去处理。

const delNode = (del: FlowNode) => {
  //删除当前节点需要维护当前节点的父节点和该节点的子节点关系
  delete nodesError.value[del.id];
  delNodeNext(props.process, del);
};

//删除节点的错误信息
const delError = (node: FlowNode) => {
  ...
};

const delNodeNext = (next: FlowNode, del: FlowNode) => {
  delete nodesError.value[del.id];
  //常规链表删除链接的操作,找到删除节点的前一个节点
  if (next.id === del.pid) {
    //条件分支是整个大节点,里面显示的是分支节点,用户只能点击分支节点,需要通过点击分支节点去删除条件分支大节点
    if (Reflect.has(next, "children") && next.child?.id !== del.id) {
      const branchNode = next as BranchNode;
      const index = branchNode.children.findIndex((item) => item.id == del.id);
      if (index !== -1) {
        if (branchNode.children.length <= 2) {
          // 两个分支节点的情况下,是将整个条件分支删除
          delError(branchNode);
          delNode(branchNode); //重新判断删除整个分支节点
        } else {
          //添加条件 到 多个分支节点的情况下,是点击谁删谁
          delError(del);
          branchNode.children.splice(index, 1);
        }
      }
    } else {
      if (del.child && del.child.id) {
        del.child.pid = next.id;
      }
      next.child = del.child;
    }
  } else {
    //普通节点的删除情况
    if (next.child) {
      delNodeNext(next.child, del);
    }
    //条件分支的删除情况,会把子节点和自身全部删除
    if (Reflect.has(next, "children")) {
      const nextBranch = next as BranchNode;
      if (nextBranch.children && nextBranch.children.length) {
        nextBranch.children.forEach((item) => {
          delNodeNext(item, del);
        });
      }
    }
  }
};

这里展示不看处理分支节点的情况。以普通节点为例,当找到的时候,需要进行关系的重新设置,重新设置删除节点子节点的父id。同时维护删除节点的父节点连接到删除节点的子节点上

      if (del.child && del.child.id) {
        del.child.pid = next.id;
      }
      next.child = del.child;

分支节点

分支节点的话处理情况比普通节点复杂一点。如图所示区域,整体作为分支总节点。其中该总分支节点的child是直接连接下一个节点的,而内部节点数据维护在自身的children属性中。
在这里插入图片描述
如下是插入分支节点的情况,分支节点插入的时候默认插入一个总节点,但是总结点不展示数据信息,同时创建两个分支节点给总节点的chidlrens中保存。

const addCondition = (node: FlowNode) => {
  const exclusive = node as ExclusiveNode;
  exclusive.children.splice(exclusive.children.length - 1, 0, {
    id: nextId(),
    pid: exclusive.id,
    type: "condition",
    def: false,
    name: `条件${exclusive.children.length + 1}`,
    conditions: {
      operator: "and",
      conditions: [],
      groups: [],
    } as FilterRules,
    child: undefined,
  });
};

const addExclusive = (node: FlowNode) => {
  //node是父节点
  const child = node.child; //进行链接操作 ,如果是初始化情况,node.child链接的是结束节点
  const id = nextId();
  const exclusiveNode = {
    //创建条件分支节点主体数据
    id: id,
    pid: node.id,
    type: "exclusive",
    name: "独占网关",
    child: child,
    children: [],
  } as ExclusiveNode;
  if (child) child.pid = id; //修改原子节点中的保存的父节点的id
  //执行两次是创建条件分支自己的两个内部节点
  addCondition(exclusiveNode);
  addCondition(exclusiveNode);
  node.child = exclusiveNode; //重新连接父节点的字节点

  if (exclusiveNode.children.length > 0) {
    //修改默认创建的两个条件子节点中最后一个的展示数据
    const condition = exclusiveNode.children[
      exclusiveNode.children.length - 1
    ] as ConditionNode;
    condition.def = true;
    condition.name = "默认条件";
  }
};

exclusive节点如下,GatewayNode 充当整个最大的分支节点,

  <GatewayNode v-bind="$attrs" :node="node">
    <!-- 使用作用域插槽 -->
    <template #default="{ addNode, readOnly }">
      <el-button
        type="primary"
        :disabled="readOnly"
        @click="addNode('condition', node)"
        plain
        round
        >添加条件
      </el-button>
    </template>
  </GatewayNode>

GatewayNode 组件代码如下

  <div class="gateway-node">
    <!-- 替换默认的添加节点 -->
    <div class="add-branch">
      <slot :addNode="addNode" :readOnly="readOnly" />
    </div>
    <div v-for="(item, index) in node.children" :key="item.id" class="col-box">
      <template v-if="index === 0">
        <div class="top-left-border"></div>
        <div class="bottom-left-border" />
      </template>
      <template v-else-if="node.children.length === index + 1">
        <div class="top-right-border"></div>
        <div class="bottom-right-border" />
      </template>
      <TreeNode
        :node="item"
        v-bind="$attrs"
        @addNode="addNode"
        class="col-node"
      >
        <template #append>
          <div
            class="move-left"
            @click.stop="moveLeft(index)"
            v-show="
              index !== 0 && node.children.length !== index + 1 && !readOnly
            "
          >
            <svg-icon name="el:ArrowLeft" />
          </div>
          <div
            class="move-right"
            @click.stop="moveRight(index)"
            v-show="
              ![index + 1, index + 2].includes(node.children.length) &&
              !readOnly
            "
          >
            <svg-icon name="el:ArrowRight" />
          </div>
        </template>
      </TreeNode>
    </div>
  </div>
  <!-- 条件分支节点中的子节点还能添加节点 -->
  <Add @addNode="addNode" class="branch-but" />

最终显示的一个个小的条件分支节点ConditionNode组件如下

  <div class="branch-node">
    <Node v-bind="$attrs" icon="el:Share" :node="node" :readOnly="node.def">
      <el-text>{{ content }}</el-text>
      <slot name="append" />
    </Node>
  </div>

在删除节点分支时候主要如下代码

    if ("children" in next && next.child?.id !== del.id) {
      const branchNode = next as BranchNode;
      const index = branchNode.children.findIndex((item) => item.id == del.id);
      if (index !== -1) {
        if (branchNode.children.length <= 2) {
          // 两个分支节点的情况下,是将整个条件分支删除
          delError(branchNode);
          delNode(branchNode); //重新判断删除整个分支节点
        } else {
          //添加条件 到 多个分支节点的情况下,是点击谁删谁
          delError(del);
          branchNode.children.splice(index, 1);
        }
      }
    //条件分支的删除情况,会把子节点和自身全部删除
    if ("children" in next) {
      const nextBranch = next as BranchNode;
      if (nextBranch.children && nextBranch.children.length > 0) {
        nextBranch.children.forEach((item) => {
          delNodeNext(item, del);
        });
      }
    }

删除的时候需要处理总的分支节点的情况,因此这里将总分支中每一个子节点进行删除,若删除的子分支节点存在子数据也会一起删除

      if (nextBranch.children && nextBranch.children.length > 0) {
        nextBranch.children.forEach((item) => {
          delNodeNext(item, del);
        });
      }
  • 12
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值