应用开发平台集成工作流系列之17——流程建模功能前端设计与改造回顾

news2024/10/6 2:26:30

背景

对于流程设置不友好的问题,国内钉钉另行设计与实现了一套流程建模模式,跟bpmn规范无关,有人仿照实现了下,并做了开源(https://github.com/StavinLi/Workflow-Vue3),效果图如下:

实现大致原理是基于无限嵌套的子节点,输出json数据,传给后端,后端进行解析后,调用Camunda引擎的api,转换成流程模型后持久化。

该系列通过十几篇对Camunda工作流引擎集成做了相对比较详细的设计和实现说明工作,在集成过程中,对原前端Workflow-Vue3做了大量的改造,前面零零星星的也随着部分博客提及过,今天做一个全局性的回顾和整理。

节点类型

目前平台已经实现的节点有三类,发起节点、办理节点、路由节点。
发起节点与办理节点对应着Camunda中的用户任务类型,路由节点对应着Camunda中的兼容网关类型。
后续可以在此基础上进行功能扩展,使用Camunda的服务节点,构建不同用途的前端节点类型,如发送通知(邮件、短信等)、流程归档等。

节点配置

流程是有多个节点组成的,但仅有节点,以及连接节点的边还不够,每个节点需要进行必要的配置。
对于不同类型的节点,配置也有差异。

基础属性

基础属性是节点的通用公共数据结构,所有节点类型都一样,如下图所示:

{
	id:'',
	name: '填报',
	type: 'FIRST_NODE',
	config: {},
	branchList: [],
	child: {}
}

各属性含义如下

属性类型含义说明
idString标识唯一性标识,由前端随机生成
nameString名称节点名称
typeString类型ROOT(发起人,根节点)、HANDLE(办理)、INCLUSIVE_GATEWAY(路由)
configObject配置节点的设置项内容,不同类型的节点设置内容不同,从而数据结构不同
childObject子节点当前节点的子对象
branchListArray分支当type 为 INCLUSIVE_GATEWAY 时,该项存在,存放分支数据

办理人设置

针对用户任务节点,设置该环节是会签还是单人处理,是否需要用户指定处理人,以及关联的用户组(角色)。
数据结构示例:

	"personConfig": {
				"mode": "NORMAL",
				"setAssigneeFlag": "YES",
				"userGroup": "99",
				"userGroupName": "系统管理员"
	}

属性说明:

属性类型含义说明
modeString模式COUNTERSIGN:会签;NORMAL:普通
setAssigneeFlagString是否指定人YES:是;NO:否
userGroupString用户组(角色)调用平台的用户组单选组件,选择一个用户组,用户组的标识
userGroupNameString用户组(角色)的名称用户组的名称

表单权限设置

控制该环节表单不同区域的可见性与可编辑性,对发起节点(流程首环节,通常用于填报)和用户任务节点需要设置。

数据结构示例:

	"config": {
		"permissionConfig": [{
			"areaCode": "applyArea",
			"permission": "EDITABLE"
		}, {
			"areaCode": "organizationApproval",
			"permission": "READONLY"
		}, {
			"areaCode": "hrApproval",
			"permission": "INVISIBLE"
		}]
	}

属性说明:

属性类型含义说明
areaCodeString区域编码在平台权限项管理中定义与维护
permissionString权限编码INVISIBLE:不可见;READONLY:只读;EDITABLE:可编辑

回退节点设置

当前环节可回退到的节点定义。

数据结构示例:

	"backNodeList": [{
					"id": "root",
					"name": "填报"
				}, {
					"id": "node1938_8b28_c3ed_030f",
					"name": "部门审批"
	}]

属性说明:

属性类型含义说明
idString环节标识发起环节使用统一约定的root
nameString环节名称

跳转节点设置

当前环节可跳转到的节点定义,与回退节点配置类似。

数据结构示例:

	"jumpNodeList": [{
					"id": "root",
					"name": "填报"
				}, {
					"id": "node1938_8b28_c3ed_030f",
					"name": "部门审批"
	}]

属性说明:

属性类型含义说明
idString环节标识发起环节使用统一约定的root
nameString环节名称

监听器配置

环节监听器的配置,用于实现特定的业务逻辑

数据结构示例:

	"listenerList": [{
				"category": "TASK",
				"type": "CLASS",
				"name": "请假申请部门审批完成",
				"code": "tech.abc.platform.businessflow.listener.LeaveDepartApprovalCompleteListener",
				"event": "COMPLETE",
				"eventName": "完成"
			}, {
				"category": "TASK",
				"type": "CLASS",
				"name": "请假申请部门审批完成",
				"code": "tech.abc.platform.businessflow.listener.LeaveDepartApprovalCompleteListener",
				"event": "COMPLETE",
				"eventName": "完成"
}]

属性说明:

属性类型含义说明
categoryString分类TASK:任务监听器;EXECUTION:执行监听器
typeString类型CLASS:类;EXPRESSION:表达式;DELEGATE_EXPRESSION:委托表达式
nameString名称监听器名称
codeString编码取决于type,如为类,则本属性放完整的类名
eventString事件编码取决于category,如为任务监听器,则取值如下:
CREATE:创建;ASSIGNMENT:指派;COMPLETE:完成;DELETE:删除
eventNameString事件名称参见上面事件编码的枚举值定义中文部分

条件配置

设置条件,路由节点后续的分支节点需要配置。

数据结构示例:

{
	"name": "路由",
	"id": "node3278_00b0_e238_a105",
	"type": "INCLUSIVE_GATEWAY",
	"config": {},
	"child": null,
	"branchList": [{
			"name": "3天以内",
			"id": "condition5914_12fb_e783_f171",
			"type": "CONDITION",
			"config": {
				"expression": "${total<=3}"
			},
			"branchList": []
		},
		{
			"name": "超过3天",
			"id": "condition10081_fd56_1fb6_f8ed",
			"type": "CONDITION",
			"config": {
				"expression": "${total>3}"
			},
			"branchList": []
		}
	]
}

属性说明:

属性类型含义说明
expressionString表达式示例:${total<=3}

组件改造

如上面所示,我们对前端数据结构进行了大调整,相应的,原组件也需要进行相应的调整,差不多只留了架子,内部大换血,主要实现如下:

流程建模组件nodeWrap

效果图
image.png

源码

<!-- eslint-disable vue/no-mutating-props -->
<!--
 * @Date: 2022-09-21 14:41:53
 * @LastEditors: StavinLi 495727881@qq.com
 * @LastEditTime: 2023-05-24 15:20:24
 * @FilePath: /Workflow-Vue3/src/components/nodeWrap.vue
-->
<template>
  <!-- 发起环节 -->
  <div class="node-wrap" v-if="modelValue.type == 'ROOT'">
    <div
      class="node-wrap-box"
      :class="'start-node' + (isTried && modelValue.error ? 'active error' : '')"
    >
      <div class="title" :style="`background: rgb(87, 106, 149);`">
        <span v-if="modelValue.type == 'ROOT'">{{ modelValue.name }}</span>
      </div>
      <div class="content" @click="setRootNode">
        <div class="text"> 发起环节 </div>
        <i class="anticon anticon-right arrow"></i>
      </div>
      <div class="error_tip" v-if="isTried && modelValue.error">
        <i class="anticon anticon-exclamation-circle"></i>
      </div>
    </div>
    <addNode v-model:childNodeP="modelValue.child" />
  </div>
  <!-- 办理环节  -->
  <div class="node-wrap" v-else-if="modelValue.type == 'HANDLE'">
    <div class="node-wrap-box" :class="isTried && modelValue.error ? 'active error' : ''">
      <div class="title" :style="`background: rgb(255, 148, 62);`">
        <input
          v-if="isInput"
          type="text"
          class="ant-input editable-title-input"
          @blur="blurEvent()"
          @focus="$event.currentTarget.select()"
          v-focus
          v-model="modelValue.name"
        />
        <span v-else class="editable-title" @click="clickEvent()">{{ modelValue.name }}</span>
        <i class="anticon anticon-close close" @click="delNode"></i>
      </div>
      <div class="content" @click="setHandleNode">
        <div class="text">
          <span
            class="placeholder"
            v-if="!modelValue.config.personConfig || !modelValue.config.personConfig.userGroupName"
            >待配置</span
          >
          <template v-else> {{ modelValue.config.personConfig.userGroupName }}</template>
        </div>
        <i class="anticon anticon-right arrow"></i>
      </div>
      <div class="error_tip" v-if="isTried && modelValue.error">
        <i class="anticon anticon-exclamation-circle"></i>
      </div>
    </div>
    <addNode v-model:childNodeP="modelValue.child" />
  </div>
  <!-- 路由节点 -->
  <div class="branch-wrap" v-else-if="modelValue.type == 'INCLUSIVE_GATEWAY'">
    <div class="branch-box-wrap">
      <div class="branch-box">
        <button class="add-branch" @click="addCondition">添加条件</button>
        <div class="col-box" v-for="(item, index) in modelValue.branchList" :key="index">
          <div class="condition-node">
            <div class="condition-node-box">
              <div class="auto-judge" :class="isTried && item.error ? 'error active' : ''">
                <div class="sort-left" v-if="index != 0" @click="arrTransfer(index, -1)">&lt;</div>
                <div class="title-wrapper">
                  <input
                    v-if="isInputList[index]"
                    type="text"
                    class="ant-input editable-title-input"
                    @blur="blurEvent(index)"
                    @focus="$event.currentTarget.select()"
                    v-focus
                    v-model="item.name"
                  />
                  <span v-else class="editable-title" @click="clickEvent(index)">{{
                    item.name
                  }}</span>
                  <i class="anticon anticon-close close" @click="removeCondition(index)"></i>
                </div>
                <div
                  class="sort-right"
                  v-if="index != modelValue.branchList.length - 1"
                  @click="arrTransfer(index)"
                  >&gt;</div
                >
                <div class="content" @click="setConditionNode(item)">
                  <span class="placeholder" v-if="!item.config.expression">未设置</span>
                  {{ item.config.expression }}
                </div>
                <div class="error_tip" v-if="isTried && item.error">
                  <i class="anticon anticon-exclamation-circle"></i>
                </div>
              </div>
              <addNode v-model:childNodeP="item.child" />
            </div>
          </div>
          <nodeWrap v-if="item.child" v-model:modelValue="item.child" />
          <template v-if="index == 0">
            <div class="top-left-cover-line"></div>
            <div class="bottom-left-cover-line"></div>
          </template>
          <template v-if="index == modelValue.branchList.length - 1">
            <div class="top-right-cover-line"></div>
            <div class="bottom-right-cover-line"></div>
          </template>
        </div>
      </div>
      <addNode v-model:childNodeP="modelValue.child" />
    </div>
  </div>
  <nodeWrap v-if="modelValue.child" v-model:modelValue="modelValue.child" />
</template>
<script setup>
import { onMounted, ref, watch, getCurrentInstance, computed } from 'vue'
import addNode from './addNode.vue'
import $func from '../utils/index'
import { useStore } from '../stores/index'

let _uid = getCurrentInstance().uid

let props = defineProps({
  modelValue: {
    type: Object,
    default: () => ({})
  }
})

let isInputList = ref([])
let isInput = ref(false)
const resetConditionNodesErr = () => {
  for (var i = 0; i < props.modelValue.branchList.length; i++) {
    // eslint-disable-next-line vue/no-mutating-props
    props.modelValue.branchList[i].error =
      $func.conditionStr(props.modelValue, i) == '请设置条件' &&
      i != props.modelValue.branchList.length - 1
  }
}
onMounted(() => {})
let emits = defineEmits(['update:modelValue'])
let store = useStore()
let {
  setRootNodeConfigVisible,
  setRootNodeConfig,
  setHandleNodeConfigVisible,
  setHandleNodeConfig,
  setConditionNodeConfigVisible,
  setConditionNodeConfig
} = store
let isTried = computed(() => store.isTried)

const clickEvent = (index) => {
  if (index || index === 0) {
    isInputList.value[index] = true
  } else {
    isInput.value = true
  }
}
const blurEvent = (index) => {
  if (index || index === 0) {
    isInputList.value[index] = false
    // eslint-disable-next-line vue/no-mutating-props
    props.modelValue.branchList[index].name = props.modelValue.branchList[index].name || '条件'
  } else {
    isInput.value = false
    // eslint-disable-next-line vue/no-mutating-props
    props.modelValue.name = props.modelValue.name
  }
}
const delNode = () => {
  emits('update:modelValue', props.modelValue.child)
}
const addCondition = () => {
  let len = props.modelValue.branchList.length + 1
  // eslint-disable-next-line vue/no-mutating-props
  props.modelValue.branchList.push({
    name: '条件' + len,
    type: 'CONDITION',
    child: null
  })
  resetConditionNodesErr()
  emits('update:modelValue', props.modelValue)
}
const removeCondition = (index) => {
  // eslint-disable-next-line vue/no-mutating-props
  props.modelValue.branchList.splice(index, 1)
  props.modelValue.branchList.map((item, index) => {
    item.name = `条件${index + 1}`
  })
  resetConditionNodesErr()
  emits('update:modelValue', props.modelValue)
  if (props.modelValue.branchList.length == 1) {
    if (props.modelValue.child) {
      if (props.modelValue.branchList[0].child) {
        reData(props.modelValue.branchList[0].child, props.modelValue.child)
      } else {
        // eslint-disable-next-line vue/no-mutating-props
        props.modelValue.branchList[0].child = props.modelValue.child
      }
    }
    emits('update:modelValue', props.modelValue.branchList[0].child)
  }
}
const reData = (data, addData) => {
  if (!data.child) {
    data.child = addData
  } else {
    reData(data.child, addData)
  }
}

const arrTransfer = (index, type = 1) => {
  //向左-1,向右1
  // eslint-disable-next-line vue/no-mutating-props
  props.modelValue.branchList[index] = props.modelValue.branchList.splice(
    index + type,
    1,
    props.modelValue.branchList[index]
  )[0]
  props.modelValue.branchList.map((item, index) => {
    item.priorityLevel = index + 1
  })
  resetConditionNodesErr()
  emits('update:modelValue', props.modelValue)
}

// 设置发起环节配置
const setRootNode = () => {
  setRootNodeConfigVisible(true)
  const nodeConfig = {
    config: props.modelValue.config,
    flag: false,
    componentId: _uid,
    id: props.modelValue.id,
    model: props.modelValue
  }

  setRootNodeConfig(nodeConfig)
}
let rootNodeConfig = computed(() => store.rootNodeConfig)
watch(
  rootNodeConfig,
  (myConfig) => {
    if (myConfig.flag && myConfig.componentId === _uid) {
      const modelValue = Object.assign(props.modelValue, { config: myConfig.config })
      emits('update:modelValue', modelValue)
    }
  },
  { deep: true }
)

// 设置办理环节配置
const setHandleNode = () => {
  setHandleNodeConfigVisible(true)
  const handleNodeConfig = {
    config: props.modelValue.config,
    flag: false,
    componentId: _uid,
    id: props.modelValue.id,
    model: props.modelValue
  }
  setHandleNodeConfig(handleNodeConfig)
}

let handleNodeConfig = computed(() => store.handleNodeConfig)
watch(
  handleNodeConfig,
  (myConfig) => {
    if (myConfig.flag && myConfig.componentId === _uid) {
      const modelValue = Object.assign(props.modelValue, { config: myConfig.config })
      emits('update:modelValue', modelValue)
    }
  },
  { deep: true }
)

// 设置条件节点配置
const setConditionNode = (condition) => {
  setConditionNodeConfigVisible(true)
  const conditionNodeConfig = {
    ...condition.config,
    flag: false,
    componentId: _uid,
    nodeId: condition.id
  }
  setConditionNodeConfig(conditionNodeConfig)
}

let conditionNodeConfig = computed(() => store.conditionNodeConfig)
watch(
  conditionNodeConfig,
  (myConfig) => {
    if (myConfig.flag && myConfig.componentId === _uid) {
      // 只保留必要属性,移除辅助使用的componentId和flag
      const conditionNodeConfig = {
        expression: myConfig.expression
      }
      // 获取条件节点标识
      const conditionNodeId = myConfig.nodeId
      let newModelValue = props.modelValue

      newModelValue.branchList.forEach((element) => {
        if (element.id === conditionNodeId) {
          element.config = conditionNodeConfig
          return
        }
      })

      emits('update:modelValue', newModelValue)
    }
  },
  { deep: true }
)
</script>
<style scoped>
@import '../css/workflow.css';
.error_tip {
  position: absolute;
  top: 0px;
  right: 0px;
  transform: translate(150%, 0px);
  font-size: 24px;
}

.promoter_person .el-dialog__body {
  padding: 10px 20px 14px 20px;
}

.selected_list {
  margin-bottom: 20px;
  line-height: 30px;
}

.selected_list span {
  margin-right: 10px;
  padding: 3px 6px 3px 9px;
  line-height: 12px;
  white-space: nowrap;
  border-radius: 2px;
  border: 1px solid rgba(220, 220, 220, 1);
}

.selected_list img {
  margin-left: 5px;
  width: 7px;
  height: 7px;
  cursor: pointer;
}
</style>

添加节点组件addNode

效果图
image.png

源码

<template>
  <div class="add-node-btn-box">
    <div class="add-node-btn">
      <el-popover placement="right-start" v-model="visible" width="auto">
        <div class="add-node-popover-body">
          <a class="add-node-popover-item approver" @click="addHandle()">
            <div class="item-wrapper">
              <span class="iconfont"></span>
            </div>
            <p>办理节点</p>
          </a>
          <a class="add-node-popover-item condition" @click="addConditionBranch">
            <div class="item-wrapper">
              <span class="iconfont"></span>
            </div>
            <p>路由分支</p>
          </a>
        </div>
        <template #reference>
          <button class="btn" type="button">
            <span class="iconfont"></span>
          </button>
        </template>
      </el-popover>
    </div>
  </div>
</template>
<script setup>
import { ref } from 'vue'
import { uuid } from '@/utils'

let props = defineProps({
  childNodeP: {
    type: Object,
    default: () => ({})
  }
})
let emits = defineEmits(['update:childNodeP'])
let visible = ref(false)
const addHandle = () => {
  visible.value = false
  const data = {
    name: '办理环节',
    id: 'node' + uuid(),
    type: 'HANDLE',
    config: {
      personConfig: {},
      permissionConfig: []
    },
    child: {}
  }
  emits('update:childNodeP', data)
}
const addConditionBranch = () => {
  visible.value = false
  const data = {
    name: '路由',
    id: 'node' + uuid(),
    type: 'INCLUSIVE_GATEWAY',
    config: {},
    child: null,
    branchList: [
      {
        name: '条件1',
        id: 'condition' + uuid(),
        type: 'CONDITION',
        config: {},
        branchList: [],
        child: props.childNodeP
      },
      {
        name: '条件2',
        id: 'condition' + uuid(),
        type: 'CONDITION',
        config: {},
        branchList: []
      }
    ]
  }
  emits('update:childNodeP', data)
}
</script>
<style scoped lang="less">
.add-node-btn-box {
  width: 240px;
  display: -webkit-inline-box;
  display: -ms-inline-flexbox;
  display: inline-flex;
  -ms-flex-negative: 0;
  flex-shrink: 0;
  -webkit-box-flex: 1;
  -ms-flex-positive: 1;
  position: relative;
  &:before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: -1;
    margin: auto;
    width: 2px;
    height: 100%;
    background-color: #cacaca;
  }
  .add-node-btn {
    user-select: none;
    width: 240px;
    padding: 20px 0 32px;
    display: flex;
    -webkit-box-pack: center;
    justify-content: center;
    flex-shrink: 0;
    -webkit-box-flex: 1;
    flex-grow: 1;
    .btn {
      outline: none;
      box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
      width: 30px;
      height: 30px;
      background: #3296fa;
      border-radius: 50%;
      position: relative;
      border: none;
      line-height: 30px;
      -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
      .iconfont {
        color: #fff;
        font-size: 16px;
      }
      &:hover {
        transform: scale(1.3);
        box-shadow: 0 13px 27px 0 rgba(0, 0, 0, 0.1);
      }
      &:active {
        transform: none;
        background: #1e83e9;
        box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
      }
    }
  }
}
</style>
<style lang="less" scoped>
@import '../css/workflow.css';
.add-node-popover-body {
  display: flex;
  .add-node-popover-item {
    margin-right: 10px;
    cursor: pointer;
    text-align: center;
    flex: 1;
    color: #191f25 !important;
    .item-wrapper {
      user-select: none;
      display: inline-block;
      width: 80px;
      height: 80px;
      margin-bottom: 5px;
      background: #fff;
      border: 1px solid #e2e2e2;
      border-radius: 50%;
      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
      .iconfont {
        font-size: 35px;
        line-height: 80px;
      }
    }
    &.approver {
      .item-wrapper {
        color: #ff943e;
      }
    }
    &.notifier {
      .item-wrapper {
        color: #3296fa;
      }
    }
    &.condition {
      .item-wrapper {
        color: #15bc83;
      }
    }
    &:hover {
      .item-wrapper {
        background: #3296fa;
        box-shadow: 0 10px 20px 0 rgba(50, 150, 250, 0.4);
      }
      .iconfont {
        color: #fff;
      }
    }
    &:active {
      .item-wrapper {
        box-shadow: none;
        background: #eaeaea;
      }
      .iconfont {
        color: inherit;
      }
    }
  }
}
</style>

发起环节配置

发起环节相对比较特殊,不需要设置处理人员和回退环节,只需要设置表单权限和跳转环节。
image.png

源码

<template>
  <el-drawer
    :append-to-body="true"
    title="环节设置"
    v-model="visible"
    :show-close="false"
    :size="550"
    :before-close="close"
    destroy-on-close
  >
    <el-collapse v-model="activeName">
      <el-collapse-item title="权限设置" name="permissionConfig">
        <el-table :data="permissionData" style="width: 100%" highlight-current-row border>
          <el-table-column label="区域" width="120">
            <template #default="scope">{{ scope.row.areaName }}</template>
          </el-table-column>
          <el-table-column label="权限">
            <template #default="scope">
              <dictionary-radio-group
                v-model="scope.row.permission"
                code="NodePermissionCode"
                class="form-item"
              />
            </template>
          </el-table-column>
        </el-table>
      </el-collapse-item>

      <el-collapse-item title="跳转环节" name="jumpNodeListConfig">
        <div class="mb-10px mt-10px">
          <el-button type="primary" icon="plus" @click="addJump">新增</el-button>
        </div>
        <el-table :data="jumpNodeList" style="width: 100%" highlight-current-row border>
          <el-table-column label="环节名称">
            <template #default="scope">{{ scope.row.name }}</template>
          </el-table-column>
          <el-table-column fixed="right" label="操作" width="90">
            <template #default="scope">
              <el-button type="primary" @click="removeJump(scope.row)">移除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </el-collapse-item>
    </el-collapse>

    <FlowStepSelect ref="flowStepSelectJump" @update="updateJump" />
    <template #footer>
      <el-button type="primary" @click="save">确 定</el-button>
      <el-button @click="close">取 消</el-button>
    </template>
  </el-drawer>
</template>
<script>
import DictionaryRadioGroup from '@/components/abc/DictionarySelect/DictionaryRadioGroup.vue'
import FlowStepSelect from '../dialog/FlowStepSelect.vue'
import { useStore } from '../../stores/index'
let store = useStore()
export default {
  components: { DictionaryRadioGroup, FlowStepSelect },
  data() {
    return {
      activeName: ['permissionConfig', 'jumpNodeListConfig'],
      // 权限数据
      permissionData: [],

      // 跳转环节
      jumpNodeList: []
    }
  },
  computed: {
    visible() {
      return store.rootNodeConfigVisible
    },
    rootNodeConfig() {
      return store.rootNodeConfig
    },
    processDefinitionId() {
      return store.processDefinitionId
    }
  },
  watch: {
    rootNodeConfig(value) {
      // 加载权限设置
      this.$api.workflow.workflowNodePermissionConfig
        .getNodePermissionConfig(this.processDefinitionId, value.id)
        .then((res) => {
          if (res.data) {
            this.permissionData = res.data
            // 根据配置更新
            const permissionConfig = value.config.permissionConfig
            if (permissionConfig && permissionConfig.length > 0) {
              this.permissionData.forEach((item) => {
                permissionConfig.forEach((config) => {
                  if (config.areaCode == item.areaCode) {
                    item.permission = config.permission
                    return
                  }
                })
              })
            }
          }
        })
      // 加载回退环节列表
      this.backNodeList = value.config.backNodeList
      // 加载跳转环节列表
      this.jumpNodeList = value.config.jumpNodeList
    }
  },
  methods: {
    close() {
      store.setRootNodeConfigVisible(false)
    },
    save() {
      const permissionConfig = this.permissionData.map((item) => {
        return {
          areaCode: item.areaCode,
          permission: item.permission
        }
      })

      const nodeConfig = Object.assign(
        store.rootNodeConfig,
        {
          config: {
            permissionConfig: permissionConfig,
            backNodeList: this.backNodeList,
            jumpNodeList: this.jumpNodeList
          }
        },
        { flag: true }
      )

      store.setRootNodeConfig(nodeConfig)
      this.close()
    },
    addJump() {
      this.$refs.flowStepSelectJump.init(this.rootNodeConfig.model, this.rootNodeConfig.id, false)
    },
    updateJump(jumpNodeList) {
      if (this.jumpNodeList) {
        const idList = this.jumpNodeList.map((item) => item.id)
        jumpNodeList.forEach((item) => {
          if (!idList.includes(item.id)) {
            this.jumpNodeList.push(item)
          }
        })
      } else {
        this.jumpNodeList = jumpNodeList
      }
    },
    removeJump(row) {
      // 找到要移除的元素的索引
      let index = this.jumpNodeList.findIndex((item) => item.id === row.id)

      // 使用splice方法移除该元素
      if (index !== -1) {
        this.jumpNodeList.splice(index, 1)
      }
    }
  }
}
</script>
<style scoped></style>

办理环节配置

办理环节要设置多项配置。
image.png

源码

<template>
  <el-drawer
    :append-to-body="true"
    title="环节设置"
    v-model="visible"
    :show-close="false"
    :size="550"
    :before-close="close"
    destroy-on-close
  >
    <el-collapse v-model="activeName" style="padding: 0">
      <el-collapse-item title="人员设置" name="personConfig">
        <el-form
          ref="form"
          :model="entityData"
          :rules="rules"
          label-width="120px"
          label-position="right"
          style="width: 90%; margin: 0px auto"
        >
          <!--表单区域 -->
          <el-form-item label="模式" prop="mode">
            <dictionary-radio-group v-model="entityData.mode" code="NodeMode" />
          </el-form-item>
          <el-form-item label="指定处理人" prop="setAssigneeFlag">
            <dictionary-radio-group v-model="entityData.setAssigneeFlag" code="YesOrNo" />
          </el-form-item>
          <el-form-item label="用户组" prop="userGroup">
            <UserGroupReference v-model="entityData.userGroup" @my-change="userGroupchange" />
          </el-form-item>
          <el-form-item label="用户组名称" prop="userGroupName" v-show="false">
            <el-input v-model="entityData.userGroupName" />
          </el-form-item>
        </el-form>
      </el-collapse-item>

      <el-collapse-item title="权限设置" name="permissionConfig">
        <el-table :data="permissionData" style="width: 100%" highlight-current-row border>
          <el-table-column label="区域" width="120">
            <template #default="scope">{{ scope.row.areaName }}</template>
          </el-table-column>
          <el-table-column label="权限">
            <template #default="scope">
              <dictionary-radio-group
                v-model="scope.row.permission"
                code="NodePermissionCode"
                class="form-item"
              />
            </template>
          </el-table-column>
        </el-table>
      </el-collapse-item>
      <el-collapse-item title="回退环节" name="backNodeListConfig">
        <div class="mb-10px mt-10px">
          <el-button type="primary" icon="plus" @click="addBack">新增</el-button>
        </div>
        <el-table :data="backNodeList" style="width: 100%" highlight-current-row border>
          <el-table-column label="环节名称">
            <template #default="scope">{{ scope.row.name }}</template>
          </el-table-column>
          <el-table-column fixed="right" label="操作" width="90">
            <template #default="scope">
              <el-button type="primary" @click="removeBack(scope.row)">移除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </el-collapse-item>
      <el-collapse-item title="跳转环节" name="jumpNodeListConfig">
        <div class="mb-10px mt-10px">
          <el-button type="primary" icon="plus" @click="addJump">新增</el-button>
        </div>
        <el-table :data="jumpNodeList" style="width: 100%" highlight-current-row border>
          <el-table-column label="环节名称">
            <template #default="scope">{{ scope.row.name }}</template>
          </el-table-column>
          <el-table-column fixed="right" label="操作" width="90">
            <template #default="scope">
              <el-button type="primary" @click="removeJump(scope.row)">移除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </el-collapse-item>
      <el-collapse-item title="监听器" name="listenerListConfig">
        <div class="mb-10px mt-10px">
          <el-button type="primary" icon="plus" @click="addListener">新增</el-button>
        </div>
        <el-table :data="listenerList" style="width: 100%" highlight-current-row border>
          <el-table-column label="事件">
            <template #default="scope">{{ scope.row.eventName }}</template>
          </el-table-column>
          <el-table-column label="名称">
            <template #default="scope">{{ scope.row.name }}</template>
          </el-table-column>

          <el-table-column fixed="right" label="操作" width="90">
            <template #default="scope">
              <el-button type="primary" @click="removeListener(scope.row)">移除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </el-collapse-item>
    </el-collapse>
    <FlowStepSelect ref="flowStepSelectBack" @update="updateBack" />
    <FlowStepSelect ref="flowStepSelectJump" @update="updateJump" />
    <FlowListenerSelect ref="flowListenerSelect" @update="updateListener" />

    <template #footer>
      <el-button type="primary" @click="save">确 定</el-button>
      <el-button @click="close">取 消</el-button>
    </template>
  </el-drawer>
</template>
<script>
import DictionaryRadioGroup from '@/components/abc/DictionarySelect/DictionaryRadioGroup.vue'
import UserGroupReference from '@/modules/system/view/userGroup/treeReference.vue'
import FlowStepSelect from '../dialog/FlowStepSelect.vue'
import FlowListenerSelect from '../dialog/FlowListenerSelect.vue'
import { useStore } from '../../stores/index'
let store = useStore()
const MODULE_CODE = 'workflow'
const ENTITY_TYPE = 'workflowNodeConfig'
export default {
  name: ENTITY_TYPE + '-modify',
  components: { DictionaryRadioGroup, UserGroupReference, FlowStepSelect, FlowListenerSelect },
  props: {
    modelData: {
      type: Object
    }
  },
  data() {
    return {
      entityType: ENTITY_TYPE,
      moduleCode: MODULE_CODE,
      // eslint-disable-next-line no-eval
      api: eval('this.$api.' + MODULE_CODE + '.' + ENTITY_TYPE),
      pageCode: MODULE_CODE + ':' + ENTITY_TYPE + ':',
      entityData: {},
      rules: {
        //前端验证规则
        mode: [{ required: true, message: '【模式】不能为空', trigger: 'blur' }],
        setAssigneeFlag: [{ required: true, message: '【指定处理人】不能为空', trigger: 'blur' }],
        userGroup: [{ required: true, message: '【用户组】不能为空', trigger: 'blur' }]
      },
      activeName: [
        'personConfig',
        'permissionConfig',
        'backNodeListConfig',
        'jumpNodeListConfig',
        'listenerListConfig'
      ],
      // 权限数据
      permissionData: [],
      // 回退环节
      backNodeList: [],
      // 跳转环节
      jumpNodeList: [],
      // 监听器
      listenerList: []
    }
  },
  computed: {
    visible() {
      return store.handleNodeConfigVisible
    },
    handleNodeConfig() {
      return store.handleNodeConfig
    }
  },
  watch: {
    handleNodeConfig(value) {
      // 加载人员设置
      if (value.config.personConfig) {
        this.entityData = value.config.personConfig
      }

      // 加载权限设置
      const processDefinitionId = store.processDefinitionId
      this.$api.workflow.workflowNodePermissionConfig
        .getNodePermissionConfig(processDefinitionId, value.id)
        .then((res) => {
          if (res.data) {
            this.permissionData = res.data
            // 根据配置更新
            const permissionConfig = value.config.permissionConfig
            if (permissionConfig && permissionConfig.length > 0) {
              this.permissionData.forEach((item) => {
                permissionConfig.forEach((config) => {
                  if (config.areaCode == item.areaCode) {
                    item.permission = config.permission
                    return
                  }
                })
              })
            }
          }
        })
      // 加载回退环节列表
      this.backNodeList = value.config.backNodeList
      // 加载跳转环节列表
      this.jumpNodeList = value.config.jumpNodeList
      // 加载监听器列表
      this.listenerList = value.config.listenerList || []
    }
  },
  methods: {
    close() {
      store.setHandleNodeConfigVisible(false)
    },
    save() {
      this.$refs.form.validate((valid) => {
        if (valid) {
          const permissionConfig = this.permissionData.map((item) => {
            return {
              areaCode: item.areaCode,
              permission: item.permission
            }
          })
          const nodeConfig = Object.assign(
            store.handleNodeConfig,
            {
              config: {
                personConfig: this.entityData,
                permissionConfig: permissionConfig,
                backNodeList: this.backNodeList,
                jumpNodeList: this.jumpNodeList,
                listenerList: this.listenerList
              }
            },
            { flag: true }
          )
          store.setHandleNodeConfig(nodeConfig)
          this.close()
        }
      })
    },
    userGroupchange(id, name) {
      this.entityData.userGroupName = name
    },
    addBack() {
      this.$refs.flowStepSelectBack.init(this.modelData, this.handleNodeConfig.id, true)
    },
    updateBack(backNodeList) {
      if (this.backNodeList) {
        const idList = this.backNodeList.map((item) => item.id)
        backNodeList.forEach((item) => {
          if (!idList.includes(item.id)) {
            this.backNodeList.push(item)
          }
        })
      } else {
        this.backNodeList = backNodeList
      }
    },
    removeBack(row) {
      // 找到要移除的元素的索引
      let index = this.backNodeList.findIndex((item) => item.id === row.id)

      // 使用splice方法移除该元素
      if (index !== -1) {
        this.backNodeList.splice(index, 1)
      }
    },
    addJump() {
      this.$refs.flowStepSelectJump.init(this.modelData, this.handleNodeConfig.id, false)
    },
    updateJump(jumpNodeList) {
      if (this.jumpNodeList) {
        const idList = this.jumpNodeList.map((item) => item.id)
        jumpNodeList.forEach((item) => {
          if (!idList.includes(item.id)) {
            this.jumpNodeList.push(item)
          }
        })
      } else {
        this.jumpNodeList = jumpNodeList
      }
    },
    removeJump(row) {
      // 找到要移除的元素的索引
      let index = this.jumpNodeList.findIndex((item) => item.id === row.id)

      // 使用splice方法移除该元素
      if (index !== -1) {
        this.jumpNodeList.splice(index, 1)
      }
    },
    addListener() {
      // 限定只能是任务监听器
      const category = ['TASK']
      this.$refs.flowListenerSelect.init(category)
    },
    updateListener(listener) {
      this.listenerList.push(listener)
    },
    removeListener(row) {
      // 找到要移除的元素的索引
      let index = this.listenerList.findIndex((item) => item.id === row.id)

      // 使用splice方法移除该元素
      if (index !== -1) {
        this.listenerList.splice(index, 1)
      }
    }
  }
}
</script>
<style></style>

条件配置

条件配置比较简单
image.png
源码

<template>
  <el-drawer
    :append-to-body="true"
    title="条件设置"
    v-model="visible"
    :show-close="false"
    :size="550"
    :before-close="close"
    destroy-on-close
  >
    <el-form
      ref="form"
      :model="entityData"
      :rules="rules"
      label-width="120px"
      label-position="right"
      style="width: 90%; margin: 0px auto"
    >
      <!--表单区域 -->

      <el-form-item label="表达式" prop="expression">
        <el-input v-model="entityData.expression" type="textarea" rows="4" />
      </el-form-item>
      <el-form-item style="float: right; margin-top: 20px">
        <el-button type="primary" @click="save">确 定</el-button>
        <el-button @click="close">取 消</el-button>
      </el-form-item>
    </el-form>
  </el-drawer>
</template>
<script>
import { useStore } from '../../stores/index'
let store = useStore()

export default {
  data() {
    return {
      entityData: {},
      rules: {
        //前端验证规则
      }
    }
  },
  computed: {
    visible() {
      return store.conditionNodeConfigVisible
    },
    conditionNodeConfig() {
      return store.conditionNodeConfig
    }
  },
  watch: {
    conditionNodeConfig(value) {
      this.entityData = value
    }
  },
  methods: {
    close() {
      store.setConditionNodeConfigVisible(false)
    },
    save() {
      const nodeConfig = Object.assign(
        store.conditionNodeConfig,
        { ...this.entityData },
        { flag: true }
      )
      store.setConditionNodeConfig(nodeConfig)
      this.close()
    }
  }
}
</script>
<style scoped></style>

开发平台资料

平台名称:一二三开发平台
简介: 企业级通用开发平台
设计资料:csdn专栏
开源地址:Gitee
开源协议:MIT
开源不易,欢迎收藏、点赞、评论。

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

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

相关文章

python设计模式笔记1:创建型模式 工厂模式和抽象工厂模式

1.工厂模式 (1) 导入所需的模块&#xff08; json 和 ElementTree &#xff09;。 (2) 定义 JSON数据提取器类&#xff08; JSONDataExtractor &#xff09;。 (3) 定义 XML数据提取器类&#xff08; XMLDataExtractor &#xff09;。 (4) 添加工厂函数 dataextraction_factor…

【错误解决方案】ModuleNotFoundError: No module named ‘PeptideBuilder‘

1. 错误提示 在python程序中&#xff0c;试图导入一个不存在的模块PeptideBuilder导致的错误&#xff1a; 错误提示&#xff1a;ModuleNotFoundError: No module named PeptideBuilder 2. 解决方案 解决方案是确保你已经正确安装了PeptideBuilder模块。你可以通过pip来安装它…

操作系统章节练习

第5章 存储器管理 一. 多选题&#xff08;共8题&#xff0c;64分&#xff09; 1. (多选题, 8分)为什么在页式存储器中实现程序共享时&#xff0c;必须对共享程序给出相同的页号&#xff1f; A. 共享页号相同方便地址转换。B. 实现程序共享时&#xff0c;由于页式存储结构要求…

多线程---线程池

文章目录 什么是线程池&#xff1f;线程池的实现标准库中的线程池&#xff08;四种&#xff09;自己实现一个线程池 线程池支持的参数在实际的开发中&#xff0c;线程池的线程数如何确定&#xff1f; 什么是线程池&#xff1f; 线程诞生的原因就是进程太“重量”了。虽然线程的…

计算机网络——第一章体系结构相关习题及详细解析

1-1 在OSI参考模型中&#xff0c;自下而上第一个提供端到端服务的层次是&#xff1a; A.数据链路层 B.传输层 C.会话层 D.应用层 答案选择&#xff1a;B.传输层 即&#xff0c;在OSI参考模型中&#xff0c;自下而上第一个提供端到端服务的层次是传输层。…

【数据结构】 队列详解!庖丁解牛般细致讲解!

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; 数据结构解析 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言&#x1f324;️队列的概念剖析☁️什么是队列☁️队列的特性☁️队列的图解 &#x1…

【鸿蒙软件开发】ArkTS容器组件之Badge

文章目录 前言一、Badge组件1.1 子组件1.2 接口接口1参数 接口2参数 BadgePosition枚举说明BadgeStyle对象说明 1.3 示例代码 总结 前言 Badge组件&#xff1a;可以附加在单个组件上用于信息标记的容器组件。 一、Badge组件 可以附加在单个组件上用于信息标记的容器组件。 说…

光强的检测与控制系统设计

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、实习内容二、实习方法2.1 proteus仿真部分2.2 使用Altium designer软件绘制原理图2.2.1 工程创建2.2.2 绘制封装以及链接封装与原件原理图2.2.3检查原件原理…

python不同版本的下载安装和配置

python下载和安装 1 基础软件安装 sudo apt update sudo apt install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libreadline-dev libffi-dev libsqlite3-dev wget2 python压缩文件下载 我这里下载的是3.9.9,各位也可以根据自己需要下…

精品Python的定制化图书借阅推荐引擎设计与实现

《[含文档PPT源码等]精品基于Python的定制化图书推荐引擎设计与实现》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程、包运行成功&#xff01; 软件开发环境及开发工具&#xff1a; 开发语言&#xff1a;python 使用框架&#xff1a;Django 前端技…

【机器学习】loss损失讨论

大纲 验证集loss上升&#xff0c;准确率也上升&#xff08;即将overfitting&#xff1f;&#xff09;训练集loss一定为要为0吗 Q1. 验证集loss上升&#xff0c;准确率也上升 随着置信度的增加&#xff0c;一小部分点的预测结果是错误的&#xff08;log lik 给出了指数级的惩…

VSCode编写Unity代码自动补全配置

1.下载并安装.NET 7.0&#xff08;C#插件需要&#xff09;和.NET Framework 4.7.1&#xff08;Unity需要&#xff09; .NET 7.0下载链接&#xff1a;https://dotnet.microsoft.com/en-us/download .NET Framework 4.7.1下载链接&#xff1a;https://dotnet.microsoft.com/en-…

Python 日期和时间处理教程:datetime 模块的使用

Python 中的日期不是独立的数据类型&#xff0c;但我们可以导入一个名为 datetime 的模块来使用日期作为日期对象。 示例&#xff1a;导入 datetime 模块并显示当前日期&#xff1a; import datetimex datetime.datetime.now() print(x)日期输出 当我们执行上面示例中的代码…

Java利用Scanner类,从键盘接受一个字符串输入,该字符串包含小写字母,大写字母和数字。分别输出该字符串所包含的大写字母、小写字母和数字的个数。

题目要求&#xff1a;利用Scanner类&#xff0c;从键盘接受一个字符串输入&#xff0c;该字符串包含小写字母&#xff0c;大写字母和数字。分别输出该字符串所包含的大写字母、小写字母和数字的个数。 import java.util.Scanner;public class Demo1 {public static void main(…

算法篇 : 并查集

介绍 英文名&#xff1a;union find set 作用&#xff1a;合并集合&#xff0c;查询集合 合并&#xff1a;将有直接关系的顶点放在一个集合里面 查找&#xff1a;查询某个顶点所属的集合 集合的标志&#xff1a;用祖先点的标号作为每个集合的标识 案例 如果说将下图的集合2合并…

H5游戏源码分享-接苹果游戏拼手速

H5游戏源码分享-接苹果游戏拼手速 看看在20秒内能接多少个苹果 <html> <head><title>我是你的小苹果</title><meta charset"utf-8"/><meta name"viewport" content"initial-scale1, user-scalableno, minimum-scale…

【DevChat】智能编程助手 - 使用评测

写在前面&#xff1a;博主是一只经过实战开发历练后投身培训事业的“小山猪”&#xff0c;昵称取自动画片《狮子王》中的“彭彭”&#xff0c;总是以乐观、积极的心态对待周边的事物。本人的技术路线从Java全栈工程师一路奔向大数据开发、数据挖掘领域&#xff0c;如今终有小成…

【unity小技巧】unity排序问题的探究

文章目录 前言一、排序图层二、sorting Group的使用三、树木排序设计方法一 代码控制方法二 拆分图片方法三 透视排序1. 普通物品排序2. TileMap瓦片排序设计 完结 前言 unity的排序问题其实之前分享的项目多多少少都有提到一点&#xff0c;但是没有单独拿出来说&#xff0c;所…

常用第三方库

Moment GTC(Greenwish Mean Time)&#xff1a;格林威治时间&#xff0c;太阳时&#xff0c;精确到毫秒UTC(Universal Time Coodinated)&#xff1a;世界协调时间&#xff0c;原子种计时&#xff0c;精确到纳秒 GTC和UTC都是以0时区作为标准时间戳&#xff1a;以UTC的1970-1-1 …

天气数据可视化平台-计算机毕业设计vue

天气变幻无常&#xff0c;影响着我们生活的方方面面&#xff0c;应用天气预报信息可以及时了解天气的趋势&#xff0c;给人们的工作、生活等带来便利&#xff0c;也可以为我们为未来的事情做安排和打算&#xff0c;所以一个精准的、易读 通过利用 程序对气象网站大量的气象信息…