likes
comments
collection

【vue3】组织架构的部门的维护方案

作者站长头像
站长
· 阅读数 15

做好维护部门信息的功能

上一篇介绍了角色与权限,里面涉及一个很重要的因素——部门。 部门和权限有着密切的联系,实现角色和权限,总是绕不开部门,所以部门维护也是一个重要环节。

相关文章

企业的部门信息结构

网上找了一张图,侵删:

【vue3】组织架构的部门的维护方案

企业的部门关系一般都是树形结构,由上级部门、下级部门的形式组成。有简单的也有复杂的,跨国集团的架构想必。。。

我们在保存部门信息的时候,不能只是单条的形式,还需要记录上级部门,下级部门等信息,而前后端的表现形式又不尽相同,我们来分别看一下。

后端的部门信息结构

后端一般会把部门信息存入关系型数据库,使用 parentId 字段体现上下级关系,使用 level、path、sort 等字段进行细节描述,比如这种结构:

部门表

字段名标签类型说明
ID部门编号int主键
deptName部门名称nvarchar部门名称
parentID上级部门编号int外键,自连接
level层数int第几级的部门
path父ID集合nvarchar父ID集合,“,”号分割。便于找到所有下级部门
sort排序int同级部门的先后顺序
sortAll总排序int所有部门一起排序
其他字段

设计接口

忘记class怎么写了,用TS的形式表达一下吧。

type idType = number | string
/**
 * 后端的组织架构
 */
export interface IOrganization {
  id: idType,
  deptName: string, // 部门名称
  parentId: idType, // 上级部门ID
  level: number, // 层级
  path: string, // 父ID集合,“,”号分割
  sort: number, // 同一个父级里的先后顺序
  sortAll: number, // 部门全排列
  [id: idType]: any // 其他字段
}

(因为想在前端写转换函数)

示例

[
  {
    "id": 1,
    "deptName": "总部门",
    "level": 0,
    "parentId": 0,
    "path": "0,",
    "sort": 0,
    "sortAll": 10
  },
  {
    "id": 2,
    "deptName": "支持部门",
    "level": 1,
    "parentId": 1,
    "path": "0,1,",
    "sort": 10,
    "sortAll": 20
  },
  {
    "id": 4,
    "deptName": "人事部门",
    "level": 2,
    "parentId": 2,
    "path": "0,1,2,",
    "sort": 10,
    "sortAll": 30
  },
  {
    "id": 7,
    "deptName": "人事部二",
    "level": 3,
    "parentId": 4,
    "path": "0,1,2,4,",
    "sort": 10,
    "sortAll": 40
  },
  {
    "id": 6,
    "deptName": "人事部一",
    "level": 3,
    "parentId": 4,
    "path": "0,1,2,4,",
    "sort": 20,
    "sortAll": 50
  },
  {
    "id": 3,
    "deptName": "研发部门",
    "level": 1,
    "parentId": 1,
    "path": "0,1,",
    "sort": 20,
    "sortAll": 60
  },
  {
    "id": 5,
    "deptName": "测试部门",
    "level": 2,
    "parentId": 3,
    "path": "0,1,3,",
    "sort": 10,
    "sortAll": 70
  }
]

前端的部门信息结构

前端一般采用树形结构,比如这样:

/**
 * 前端的组织架构,照顾后端需要的数据
 */
export interface IOrganizationTree {
  id: idType,
  label: string, // 部门名称
  children?: IOrganizationTree[] // 子部门
}

对比一下,我们会发现结构明显不同,一个是单层的,一个是多层的,一个使用parentID表示上下级,一个使用子属性的形式表示,所以需要写函数进行类型转换。

示例

[
  {
    "id": 1,
    "label": "总部门",
    "children": [
      {
        "id": 2,
        "label": "支持部门",
        "children": [
          {
            "id": 4,
            "label": "人事部门",
            "children": [
              {
                "id": 7,
                "label": "人事部二",
                "children": []
              },
              {
                "id": 6,
                "label": "人事部一",
                "children": []
              }
            ]
          }
        ]
      },
      {
        "id": 3,
        "label": "研发部门",
        "children": [
          {
            "id": 5,
            "label": "测试部门",
            "children": []
          }
        ]
      }
    ]
  }
]

类型转换的函数

两种形式需要互相转换,所以写了两个函数。 并没有写成通用的形式,另外为了便于编码,用状态作为参数。

listToTree 后端列表转为树形结构

先写一个后端列表转 tree 的函数。这里利用了js的一个特性,不需要使用递归的方式,只需要遍历两次数组即可。

import type {
  IOrganizationTree,
  IOrganization,
  IOrganizationState
} from '../types/10-organization'

/**
 * 后端的结构转换为树状结构
 * @param state 状态,orgTree:树状部门信息;orgList:后端列表;orgObject:key-value的
 */
export function listToTree(state: IOrganizationState) {
  //从状态中获取部门信息
  const { orgTree, orgList, orgObject } = state

  const tmp = {} // 中转容器
  // 第一次遍历,转换为 key 的形式
  orgList.forEach((org: IOrganization) => {
    orgObject[org.id] = org // 存入 key-value,非必要
    tmp[org.id] = {
      id: org.id,
      label: org.deptName,
      children: []
    }
  })

  // 第二次遍历,寻找父节点
  orgList.forEach((org: IOrganization) => {
    // 判断父级
    if (tmp[org.parentId]) {
      // 有父级,加入 children
      tmp[org.parentId].children.push(tmp[org.id])
    } else {
      // 顶级,加入 orgTree
      orgTree.push(tmp[org.id])
    }
  })
}

treeToList 树形结构转后端列表

目前为止,没有找到不需要使用递归的方法。 好吧,我不太擅长用递归,所以总是先想办法规避。

/**
 * tree 转换为转换为后端的形式
 * @param state 状态,orgList:后端列表,目标。orgObject:key-value;currentOrg.sortAll 全排列
 * @param parentId 父级ID,第一次传 0
 * @param level 层数,第一次传 0
 * @param path 路径,第一次传 ''
 * @param treeNode 节点
 * @param parentSort 同级里的序号,第一次传 1
 */
export function treeToList(
  state: IOrganizationState, // 
  parentId: number | string,
  level: number,
  path: string,
  treeNode: IOrganizationTree,
  parentSort: number
) {

  // 把 部门信息 存入 list
  // 判断有无children 
  // 没有,结束递归
  // 有,递归

  // 设置 orgObject
  const org = state.orgObject[treeNode.id]
  org.level = level
  org.parentId = parentId
  org.path = path + parentId + ','
  org.sort = parentSort * 10
  org.sortAll = (state.currentOrg.sortAll++ ) * 10

  // 把 部门信息 存入 list
  state.orgList.push({
    id: treeNode.id,
    deptName: treeNode.label, // 部门名称
    level: level, // 层级
    parentId: parentId, // 上级部门ID
    path: org.path, // 父ID集合,“,”号分割
    sort: org.sort, // 同一个父级里的先后顺序
    sortAll: org.sortAll // 部门全排列
  })

  // 判断有无children 
  if (treeNode.children && treeNode.children.length > 0) {
    // 有,递归
    let parent1Sort = 1
    treeNode.children.forEach((node: IOrganizationTree, index: number) => {
      treeToList(state, treeNode.id, level + 1, org.path, node, index + 1)
    })
  } else {
    // 没有,退出递归
  }
}

定义状态

为了更好的在各个组件和函数之间传递数据,所以使用了“状态”的方式。 看着是不是有点像 pinia,但是并不是 pinia,而是我直接封装的一个状态管理。

import type { InjectionKey } from 'vue'
// 状态
import { defineStore, useStoreLocal } from '@naturefw/nf-state'
import type { IState } from '@naturefw/nf-state/dist/type'
// 类型
import type {
  IOrganizationTree,
  IOrganizationState
} from '../types/10-organization'
// 转换函数
import { listToTree, treeToList } from '../controller/org'
// 状态的标识,避免命名冲突
const flag = Symbol('organization') as InjectionKey<string>

/**
 * 注册局部状态
 * * 部门信息管理用的状态 : IOrganizationState & IState
 * @returns
 */
export const regOrganizationState = (): IOrganizationState & IState => {
  // 定义 角色用的状态
  const state = defineStore<IOrganizationState>(flag, {
    state: (): IOrganizationState => {
      return {
        orgTree: [], // 部门信息,绑定 tree
        orgList: [], // 部门信息,后端用的
        orgObject: {},
        currentOrg: { // 当前的部门的信息
          id: 0, // 部门ID
          num: 1, // 序号
          sortAll: 1, // 类型转换时用的排序
          info: { // 选中的节点
            id: '',
            label: ''
          } 
        }
      }
    },
    getters: {
    },
    actions: {
      /**
       * 后端列表 转为 tree,需要手动执行
       */
      async listToTree () {
        listToTree(this)
      },
      /**
       * tree 转换为 后端列表
       */
      async treeToList() {
        // 先清空
        this.orgList.length = 0
        // 重置总排序
        this.currentOrg.sortAll = 1
        // 遍历 tree 的第一级节点
        this.orgTree.forEach((org: IOrganizationTree, index: number) => {
          // 状态, 父ID,层数,path,子节点,同级排序
          treeToList(this, 0, 0, '', org, index)
        })
      }
    }
  },
  { isLocal: true }
  )
 
  return state
}

/**
 * 子组件获取状态
 */
export const getOrganizationState = (): IOrganizationState & IState => {
  return useStoreLocal<IOrganizationState & IState>(flag)
}

我喜欢使用“充血实体类”的方式使用状态,数据和方法放在一起方便使用。

所以状态里面不仅有部门信息,还有转换函数,还可以有部门初始化的方法。

实现维护部门的代码

基础知识介绍完毕,下面开始编码实现功能。

【vue3】组织架构的部门的维护方案

部门信息结构

部门信息可以使用 el-tree 来表示,因为比较方便。

  • template
  <el-tree
    :allow-drop="allowDrop"
    :allow-drag="allowDrag"
    draggable
    :data="state.orgTree"
    :node-key="nodeKey"
    :expand-on-click-node="false"
    default-expand-all
    highlight-current
    empty-text="加载中"
    @node-click="handleNodeClick"
    @check-change="checkChange"
    v-bind="$attrs"
  >
    <template #default="{ node, data }">
        <span class="custom-tree-node">
          <span>{{ data.label }}</span>
          <span>
            <org-button :node="node" :data="data"></org-button>
          </span>
        </span>
      </template>
  </el-tree>

设置相关的属性,使用 el-tree 的 slot 实现添加部门和删除部门的操作,具体代码由子组件实现。

使用 v-bind="$attrs"接收外部设置的属性。

  • ts
  import { defineComponent } from 'vue'
  import type Node from 'element-plus/es/components/tree/src/model/node'
  import type { DropType } from 'element-plus/es/components/tree/src/tree.type'
  // 类型
  import type { IOrganizationTree } from './types/10-organization'
  // 状态
  import { getOrganizationState } from './state/state-organization'

  // 组件
  import orgButton from './comp/org-button.vue'
  import orgForm from './comp/org-form.vue'

  /**
   * 组织架构的表单
   */
  export default defineComponent({
    name: 'nf-organization-form',
    inheritAttrs: false,
    components: {
      orgButton,
      orgForm
    },
    props: {
      nodeKey: {
        type: String,
        default: 'id'
      }
    },
    setup(props, context) {
      // 获取状态
      const state = getOrganizationState()
      
      // 单击节点
      const handleNodeClick = (data: IOrganizationTree) => {
        state.currentOrg.id = data[props.nodeKey]
        state.currentOrg.info = data
      }
 
      const mychange = (e) => {
        // 防止事件冒泡
        e.stopPropagation()
      }

      // 拖拽,
      
      return {
        // 状态
        state,
        // 事件
        mychange,
        handleNodeClick,
        // 拖拽
        allowDrop,
        allowDrag
      }
    }
  })

el-tree 实现了大部分功能,我们设置好属性即可,然后加上一点代码,实现其他功能。

操作按钮

做一个组件(org-button.vue)实现部门的添加、删除的功能,代码分散开,看着就不乱了。

  • template
  <a @click="append()"> 添加下级 </a>
  <a style="margin-left: 8px" @click="remove()"> 删除 </a>

其实还想加一个“添加同级”的功能,不过想想,可以通过拖拽的方式调整顺序,所以添加的时候就不用纠结了。

  • ts
// 添加部门
const append = () => {
  if (data && node) {
    const newChildTree = {
      id: new Date().valueOf(),
      label: '新部门' + state.currentOrg.num++,
      children: [] // 子部门
    }

    const org = state.orgObject[data.id]
    const newChild = {
      id: newChildTree.id,
      deptName: newChildTree.label,
      parentId: data.id, // 上级部门ID
      level: org.level + 1, // 层级
      path: org.path + '' + data.id + ',' , // 父ID集合,“,”号分割
      sort: node.data.children.length + 1, // 先设置,并不十分准确
      sortAll: 1, // 这里不好判断
      children: [] // 子部门
    }
    // key - value 结构
    state.orgObject[newChildTree.id] = newChild
    
    data.children.push(newChildTree)
  }
}

// 删除部门
const remove = () => {
  if (data && node) {
    const parent = node.parent
    const children: IOrganizationTree[] = parent.data.children || parent.data
    const index = children.findIndex((d) => d.id === data.id)
    children.splice(index, 1)
    // dataSource.value = [...dataSource.value]
  }
}

删除部门肯定没有这么简单,实际中不可能简单删除就完事了,需要做各种判断以及是否级联删除等。 不过这里就先不考虑这些复杂情况了,因为一两句也说不清。

修改部门的详细信息

这里部门信息分为两类:

  • 系统结构需要,用户不能随意修改的,比如 parentID、sort、level等。
  • 用户可以自己修改的,比如部门名称等。

本篇介绍的代码,其实都在维护第一种情况的字段,至于其他信息嘛,那就好办了,表单大家都会做。

为了方便维护,同时也是为了方便扩展,我们把修改部门信息的表单做成了组件(org-form.vue)。 这样,是不是说外部可以自己设置一个表单组件,替换掉内置的表单? 当然是可以的,这样就可以灵活的扩展部门的各种字段的维护了。

拖拽和排序

有些单位对于部门的先后顺序比较敏感,同级部门的先后必须不能错,所以我们增加了 sort 字段来表示同级部门的先后顺序,这样显示的时候就有了依据。同时可以使用拖拽的方式进行直观的调整。

好吧,拖拽的功能是el-tree 提供的,我们只是调用了一下。

      // 拖拽,
      // 总部门必须是根节点
      const allowDrop = (draggingNode: Node, dropNode: Node, type: DropType) => {
        if (dropNode.label === '总部门') {
          return false
        } else {
          return true
        }
      }
      // 总部门不能拖拽
      const allowDrag = (draggingNode: Node) => {
        return !draggingNode.label.includes('总部门')
      }

缓存数据

前端做复杂的数据操作的时候,或则短时间内不会提交的时候,实现前端缓存就比较必要了。

我们可以把正在编辑的部门信息实时保存在 localStorage 里面,这样可以避免发生意外的时候丢失数据。

使用方式

建立组件,引入维护部门信息的组件,设置状态即可。

 <org-manage
   :expand-on-click-node="false" // 可以设置 el-tree 的各种属性
 ></org-manage>
  import { orgManage, regOrganizationState } from '../../../lib-organization/main'
  const state = regOrganizationState()
  //通过状态设置部门的信息,可以设置 tree ,也可以设置list。
  state.orgList.push(...list)
  // 如果设置list的话,需要调用转换函数
  setTimeout(() => {
    state.treeToList()
  }, 200)

sortAll 有什么用途?

不知道你有没有注意到 sortAll 这个字段,为了设置好这个字段,似乎还费了不少力气。 那么 sortAll,到底可以做什么呢?

【vue3】组织架构的部门的维护方案

普通 table

比如我们想用 el-table 显示部门信息,可以这样做:

  <nf-grid-slot
    v-bind="gridMeta"
    :dataList="tableOrg"
    size="small"
  >
    <template #deptName="scope">
      <span :style="'margin-left: '+ (scope.row.level + 1) * 10 +'px'">
        {{ scope.row.deptName }}
      </span>
    </template>
  </nf-grid-slot>

nf-grid-slot 是啥?就是基于 el-table 封装的,可以依赖 json 渲染,同时还可以设置slot的列表控件。(这个不是重点)

我们先让数组按照 sortAll 进行排序,然后设置一个 slot ,依据 level 通过 css 的 margin-left 实现不同级别的部门的缩进效果。

这样一个部门信息就展示出来了。

【vue3】组织架构的部门的维护方案

普通 select

同理,我们可以使用 el-select 显示 部门信息

  <el-select v-model="currOrg">
    <el-option
      v-for="item in state.orgList"
      :key="item.id"
      :label="item.deptName"
      :value="item.id"
    >
      <span
        :style="'margin-left: '+ (item.level + 1) * 10 +'px'"
      >{{ item.deptName }}</span>
    </el-option>
  </el-select>

设置 el-option 的 slot,设置 css 即可。

path 有什么用途?

设置 path 的目的是为了方便找到所有的下级部门。因为不管是 tree 的结构,还是 后端的 parentID,想要找到所有的下级部门,都不太方便实现。但是有了path就不同了。

sql

数据库查询,可以使用 like 的方式:

select * from tbl_Dept where path like 'xxxx,%' 

js

前端可以通过数组的 filter 和字符串的 indexOf 进行查询:

 watch(() => currOrg.value, (id) => {
   const org = state.orgObject[id]
   const arr = state.orgList.filter((item) => 
         item.path.item.path.indexOf(org.path + org.id + ',') === 0)
   tableOrg.length = 0
   tableOrg.push(...arr)
 })

源码