# 组织架构模块

# 业务功能介绍

image-20210216171507942

说明:组织架构模块主要管理公司所有部门信息,支持添加、删除、编辑操作

# 基本组件布局

目标:使用element-UI组件布局组织架构的基本布局

  • 头部布局
<el-card class="tree-card">
  <!-- 用了一个行列布局 -->
  <el-row type="flex" justify="space-between" align="middle" style="height: 40px">
    <el-col :span="20">
      <span>江苏传智播客教育科技股份有限公司</span>
    </el-col>
    <el-col :span="4">
      <el-row type="flex" justify="end">
        <!-- 两个内容 -->
        <el-col>负责人</el-col>
        <el-col>
          <!-- 下拉菜单 element -->
          <el-dropdown>
            <span>
              操作<i class="el-icon-arrow-down" />
            </span>
            <!-- 下拉菜单 -->
            <el-dropdown-menu slot="dropdown">
              <el-dropdown-item>添加子部门</el-dropdown-item>
            </el-dropdown-menu>
          </el-dropdown>
        </el-col>
      </el-row>
    </el-col>
  </el-row>
</el-card>
<style scoped>
.tree-card {
  padding: 30px 30px;
  font-size:14px;
}
</style>

总结:

  • 栅格系统的基本使用
  • 栅格中可以再次嵌套栅格
  • 栅格组件支持相关的布局熟悉

# 树形组件用法

目标:熟悉树形组件的基本用法

tree组件 (opens new window)

  • 树形组件关键属性
参数 说明 类型 可选值 默认值
data 展示数据 array
props 配置选项,具体看下表 object

注意: data 中默认 label 为节点标签的文字, children 为子节点 (可以通过 props 修改默认配置 )

export default {
  name: 'Departments',
  data() {
    return {
      list: [
        {
          label: '企业部',
          children: [
            { label: '策划部' },
            { label: '游戏部' }
          ]
        },
        { label: '事件部' },
        { label: '小卖部' }
      ]
    }
  }
}
  • 那万一后台给的树形数据, 不是label 和 children 字段名呢 ?通过 props 修改默认配置
<el-tree :data="list" :props="defaultProps" />
data() {
  return {
    list: [
      {
        name: '企业部',
        children: [
          { name: '策划部' },
          { name: '游戏部' }
        ]
      },
      { name: '事件部' },
      { name: '小卖部' }
    ],
    defaultProps: {
      label: 'name',
      children: 'children'
    }
  }
}

总结:参考官方案例实现树形菜单的基本使用

data 提供数据

props 配置数据的属性名称

# 实现树形的静态组织架构

目标:基于tree组件实现静态组件组织架构效果

<el-tree :data="departs" :props="defaultProps" />
export default {
  name: 'Departments',
  data() {
    return {
      departs: [
        {
          name: '总裁办',
          manager: '曹操',
          children: [{ name: '董事会', manager: '曹丕' }]
        },
        { name: '行政部', manager: '刘备' },
        { name: '人事部', manager: '孙权' }
      ],
      defaultProps: {
        label: 'name',
        children: 'children'
      }
    }
  }
}
  • 接下来,对每个层级节点增加显示内容,此时需要用到tree的插槽
<!-- 放置一个el-tree组件 -->
<el-tree
  :data="departs"
  :props="defaultProps"
  :default-expand-all="true"
>
  <!-- 用了一个行列布局 -->
  <template #default="{ data }">
    <el-row
      type="flex"
      justify="space-between"
      align="middle"
      style="height: 40px; width: 100%;"
    >
      <el-col :span="20">
        <span>{{ data.name }}</span>
      </el-col>
      <el-col :span="4">
        <el-row type="flex" justify="end">
          <!-- 两个内容 -->
          <el-col>{{ data.manager }}</el-col>
          <el-col>
            <!-- 下拉菜单 element -->
            <el-dropdown>
              <span> 操作<i class="el-icon-arrow-down" /> </span>
              <!-- 下拉菜单 -->
              <el-dropdown-menu slot="dropdown">
                <el-dropdown-item>添加子部门</el-dropdown-item>
                <el-dropdown-item>编辑部分</el-dropdown-item>
                <el-dropdown-item>删除部门</el-dropdown-item>
              </el-dropdown-menu>
            </el-dropdown>
          </el-col>
        </el-row>
      </el-col>
    </el-row>
  </template>
</el-tree>

总结:如何定制树形菜单的布局?通过插槽定制每一行内容

本质上就是作用域插槽(scope.data表示每一行数据;scope.node表示每一行数据的相关状态属性(比如是否展开等信息))

# 回顾

  • 路由配置
    • 静态路由和动态路由的概念
    • 初始化业务模块
    • 配置组织架构的路由
    • 左侧菜单的基本架构
      • 动态组件的用法;动态组件中插槽的用法
      • v-bind的值如果是一个对象,那么对象的所有属性都会绑定到标签上
    • 拆分路由模块
    • 导入拆分的路由形成动态路由数组并且与静态路由进行合并
    • 控制左侧菜单的点击高亮
  • 组织架构
    • 熟悉组织架构的基本业务
    • 熟悉树形组件的基本使用
      • data 提供数据
      • props 用于配置数据的属性映射
    • 基于静态数据定制树形组件的布局
      • 作用域插槽:定制列表中的条目的布局
      • 获取条目数据的方式
        • v-slot=‘scope’
        • v-slot:default='scope'
        • #default='scope'
      • scope.data 条目的数据
      • scope.node 条目的状态信息

# 拆分组织架构组件

目标: 将树形的操作内容单独抽提成组件

src/views/departments/components/tree-tools.vue

  • 拆分组件
<template>
  <el-row
    type="flex"
    justify="space-between"
    align="middle"
    style="height: 40px; width: 100%;"
  >
    <el-col :span="20">
      <span>{{ nodeData.name }}</span>
    </el-col>
    <el-col :span="4">
      <el-row type="flex" justify="end">
        <!-- 两个内容 -->
        <el-col>{{ nodeData.manager }}</el-col>
        <el-col>
          <!-- 下拉菜单 element -->
          <el-dropdown>
            <span> 操作<i class="el-icon-arrow-down" /> </span>
            <!-- 下拉菜单 -->
            <el-dropdown-menu slot="dropdown">
              <el-dropdown-item>添加子部门</el-dropdown-item>
              <el-dropdown-item>编辑部分</el-dropdown-item>
              <el-dropdown-item>删除部门</el-dropdown-item>
            </el-dropdown-menu>
          </el-dropdown>
        </el-col>
      </el-row>
    </el-col>
  </el-row>
</template>

<script>
export default {
  props: {
    // 定义传入的属性
    nodeData: {
      type: Object,
      required: true
    }
  }
}
</script>
  • 在组织架构中应用组件**src/views/departments/index.vue**
<template>
  <div class="departments-container">
    <div class="app-container">
      <el-card class="tree-card">
        <!-- 用了一个行列布局 -->
        <tree-tools :node-data="company" />

        <!-- 放置一个el-tree组件 -->
        <el-tree
          :data="departs"
          :props="defaultProps"
          :default-expand-all="true"
        >
          <!-- 用了一个行列布局 -->
          <template #default="{ data }">
            <tree-tools :node-data="data" />
          </template>
        </el-tree>
      </el-card>
    </div>
  </div>
</template>
  • 上面代码中,company变量需要在data中定义
company: { name: '江苏传智播客教育科技股份有限公司', manager: '负责人' },

同时,由于在两个位置都使用了该组件,但是放置在最上层的组件是不需要显示 **删除部门编辑部门**的

所以,增加一个新的属性 **isRoot(是否根节点)**进行控制

export default {
  props: {
    // 节点的内容展示
    nodeData: {
      type: Object,
      required: true
    },
    // 是否根节点
    isRoot: {
      type: Boolean,
      default: false
    }
  }
}
<tree-tools :node-data="company" :is-root="true" />
  • 组件中, 根据isRoot判断显示
<!-- 下拉菜单 element -->
<el-dropdown>
  <span @click.stop>操作<i class="el-icon-arrow-down" /> </span>
  <!-- 下拉菜单 -->
  <el-dropdown-menu slot="dropdown">
    <el-dropdown-item>添加子部门</el-dropdown-item>
    <el-dropdown-item v-if="!isRoot">编辑部分</el-dropdown-item>
    <el-dropdown-item v-if="!isRoot">删除部门</el-dropdown-item>
  </el-dropdown-menu>
</el-dropdown>

总结:

  1. 封装基本的树形条目的组件(抽取动态属性)
  2. 导入并使用组件,然后把数据注入进去

# 配置本地接口环境

  1. 安装MongoDB数据库(不要勾选 install mongoDB compass

  2. 配置环境变量(sysdm.cpl)

  3. 启动MongoDB数据库(命令行输入 mongod;如果没有配置环境变量,需要进入数据库的安装位置运行命令 C:\Program Files\MongoDB\Server\4.2\bin)

  4. 下载后端接口代码(Word文档中有地址)

  5. 运行后端接口项目

    1. npm install
    2. npm start 初始化数据库(仅仅需要执行一次即可)
    3. npm run serve 启动项目
  6. 可以通过 http://localhost:3000/ 地址测试接口是否可以访问

  7. 修改项目中的代理的目标地址

proxy: {
  // 所有的请求路径以api开始的地址都会被代理
  // 发送的请求 http://localhost:9528/api/login
  // 代理的目标 http://ihrm-java.itheima.net/api/login
  '^/api': {
    // 代理的目标地址
    // target: 'http://ihrm-java.itheima.net'
    target: 'http://localhost:3000'
  }
}
  1. 重启前端项目

# 获取组织架构接口数据

**目标**获取真实的组织架构数据,并将其转化成树形数据显示在页面上

  • 封装API接口,获取数据**src/api/departments.js**
export function reqGetDepartments() {
  return request({
    url: '/company/department'
  })
}
  • 在钩子函数中调用接口
  import { reqGetDepartments } from '@/api/departments'
  created () {
    this.loadDepartments()
  },
  methods: {
    async loadDepartments () {
      try {
        const ret = await reqGetDepartments()
        this.company.name = ret.data.companyName
        this.departs = ret.data.depts
      } catch {
        this.$message.error('获取组织架构数据失败')
      }
    }
  }

总结:封装组织架构接口方法

注意:后端返回的数据是一个普通列表,但是我们需要树形结构的数据

# 处理数据成需要的结构

目标:转换组织架构数据

我们需要将列表型的数据,转化成树形数据,这里需要用到递归算法

我们列表型数据的关键属性: id 和 pid, id指的是自己的部门id, pid指的是父级部门的id (空则没有父级部门)

image-20210216172941882

  • 封装工具函数 src/utils/index.js
// 把列表数据结构转换为树形数据结构
// 找到当前部门(根据部门id)的所属的所有子部门
translateListToTreeData (list, id) {
  // 遍历每一个元素,并且判断每一项pid是否可以参数的pid一致,这样可以找到参数pid所属的下属部门
  const arr = []
  list.forEach(item => {
    if (item.pid === id) {
      // 如果当前部门和父级部门匹配,再去找当前部门的下级部门
      // children就是item的下级所有部门
      const children = this.translateListToTreeData(list, item.id)
      if (children.length > 0) {
        // 证明找到了当前部门的子部门
        item.children = children
      }
      arr.push(item)
    }
  })
  return arr
},
  • 调用转化方法,转化树形结构
methods: {
    // 获取原始的组织架构列表
    async loadDepartments () {
      try {
        const ret = await reqGetDepartments()
        this.company.name = ret.data.companyName
        // 把列表数据转换为树形数据
        this.departs = this.translateListToTreeData(ret.data.depts, '')
      } catch {
        this.$message.error('获取组织架构数据失败')
      }
    }
}

总结:通过递归算法实现列表转换为树形结构的功能

根据当前部门的id查询list列表中的所有子部门

# 删除部门

**目标**实现操作功能的删除功能

  1. 封装接口删除部门
  2. 绑定点击按钮的事件
  • 首先,封装删除功能模块 src/api/departments.js
export function reqDelDepartments(id) {
  return request({
    url: `/company/department/${id}`,
    method: 'delete'
  })
}
  • 然后,在tree-tools组件中,监听下拉菜单的点击事件 src/views/departments/index.vue
<!-- 下拉菜单 element -->
<el-dropdown @command="handleCommand">
  <span @click.stop>
    操作<i class="el-icon-arrow-down" />
  </span>
  <!-- 下拉菜单 -->
  <el-dropdown-menu slot="dropdown">
    <el-dropdown-item command="add">添加子部门</el-dropdown-item>
    <el-dropdown-item v-if="!isRoot" command="edit">编辑部门</el-dropdown-item>
    <el-dropdown-item v-if="!isRoot" command="del">删除部门</el-dropdown-item>
  </el-dropdown-menu>
</el-dropdown>
  • dropdown下拉菜单的监听事件command
methods: {
  handleAction (type) {
    if (type === 'add') {
      // 添加部门
    } else if (type === 'edit') {
      // 编辑部门
    } else if (type === 'del') {
      // 删除部门
      this.$confirm('确认要删除部门吗?, 是否继续?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        // 确认删除
        reqDelDepartments(this.nodeData.id)
        // 通知父组件刷新列表
        this.$emit('on-success')
      }).catch((e) => {
        if (e === 'cancel') {
          // 点击了取消
          this.$message({
            type: 'info',
            message: '已取消删除'
          })
        } else {
          // 调用接口异常
          this.$message({
            type: 'error',
            message: '删除部门失败'
          })
        }
      })
    }
  }
}

这边也可以自己注册三个点击事件, 但是自己注册需要 @click.native = "delFn"

  • 用删除接口,通知父组件更新数据

删除之前,提示用户是否删除,然后调用删除接口

但是怎么通知父组件进行更新呢? 子传父, 通知父组件

this.$emit('on-success')
  • 父组件监听事件 src/views/department/index.vue
<tree-tools @on-success='onSuccess' :node-data="scope.data" />

总结

  1. 绑定点击事件(基于Element组件进行绑定;基于原生click事件绑定)
  2. 删除需要提示
  3. 删除成功后需要通知父组件刷新列表

# 新增部门-基本功能

# 新建组件-准备弹层

我们需要构建一个新增部门的窗体组件 src/views/department/components/add-dept.vue

  • 准备弹窗组件
  • 控制弹窗的显示和隐藏
  • 弹窗的基本布局
  • 基本布局
<template>
  <!-- 放置弹层组件 -->
  <el-dialog title="新增部门" :visible="showDialog">
    我是添加的弹层
  </el-dialog>
</template>

<script>
export default {
  // 需要传入一个props变量来控制 显示或者隐藏
  props: {
    showDialog: {
      type: Boolean,
      required: true
    }
  }
}
</script>
  • 在**departments/index.vue** 中引入该组件
import AddDept from './components/add-dept'
import TreeTools from './components/tree-tools'

export default {
  components: {
    TreeTools,
    AddDept
  },
}
  • 定义控制窗体显示的变量**showDialog**
<add-dept :showDialog="showDialog"/>
data() {
  return {
    ...
    showDialog: true
  }
},

总结:

  1. index.vue把状态位传递给add-dept.vue组件
  2. 点击添加部门按钮事件发生在tree-tools.vue组件中

# 点击 x 关闭弹层 (子传父)

控制添加部门组件的弹窗打开和关闭

  1. add-dept.vue给 dialog 注册 close 事件
<el-dialog title="添加部门" :visible="showDialog" @close="handleClose">
  1. add-dept.vue子传父, 传递 false
methods: {
  handleClose() {
    // 子传父
    this.$emit('hide-addbox')
  }
}
  1. index.vue父组件中, 关闭弹层
<add-dept @hide-addbox="showDialog=false" :showDialog="showDialog"/>
<!-- 简化写法 -->
<add-dept :showDialog.sync='showDialog' />
this.$emit('update:showDialog', false)

总结:

  1. 监听el-dialog组件的close事件
  2. 事件函数中通知父组件关闭弹窗

# 准备弹层的结构内容

<template>
  <!-- 新增部门的弹层 -->
  <el-dialog title="新增部门" :visible="showDialog">
    <!-- 表单组件  el-form   label-width设置label的宽度   -->
    <!-- 匿名插槽 -->
    <el-form label-width="120px">
      <el-form-item label="部门名称">
        <el-input style="width:80%" placeholder="1-50个字符" />
      </el-form-item>
      <el-form-item label="部门编码">
        <el-input style="width:80%" placeholder="1-50个字符" />
      </el-form-item>
      <el-form-item label="部门负责人">
        <el-select style="width:80%" placeholder="请选择" />
      </el-form-item>
      <el-form-item label="部门介绍">
        <el-input style="width:80%" placeholder="1-300个字符" type="textarea" :rows="3" />
      </el-form-item>
    </el-form>
    <!-- el-dialog有专门放置底部操作栏的 插槽  具名插槽 -->
    <div slot="footer">
      <el-button type="primary" size="small">确定</el-button>
      <el-button size="small">取消</el-button>
    </div>
  </el-dialog>
</template>

# 点击[新增子部门]显示弹层组件

  • 子组件触发新增事件· src/views/departments/tree-tools.vue
if (type === 'add') {
  // 添加部门
  this.$emit('show-addbox', this.nodeData)
}
  • 父组件监听事件
<tree-tools @show-addbox="showAddbox" @on-success='onSuccess' :node-data="scope.data" />
  • 方法中弹出层,记录在哪个节点下添加子部门
data () {
  return {
    currentDept: {}
  }
}

// 处理添加操作, 显示弹层
showAddbox (data) {
  // 控制弹窗的显示
  this.showDialog = true
  // 记录当前部门的信息
  this.currentDept = data
},

总结

  1. 点击添加子部门时,需要记录当前部门的信息
  2. 这个部门信息需要传递给add-dept.vue组件,因为表单提交数据时需要父级部门信息

# 新增部门-表单验证

# 完成表单基本校验条件

表单验证条件

  • 部门名称(name):必填 1-50个字符 / 同级部门中禁止出现重复部门
  • 部门编码(code):必填 1-50个字符 / 部门编码在整个模块中都不允许重复
  • 部门负责人(manager):必填
  • 部门介绍 ( introduce):必填 1-300个字符
form: {
  name: '', // 部门名称
  code: '', // 部门编码
  manager: '', // 部门管理者
  introduce: '' // 部门介绍
},
  • 完成表单校验需要的前置条件
    • el-form配置model和rules属性
    • el-form-item配置prop属性
    • 表单进行v-model双向绑定
<template>
  <!-- 新增部门的弹层 -->
  <el-dialog title="新增部门" :visible="showDialog">
    <!-- 表单组件  el-form   label-width设置label的宽度   -->
    <!-- 匿名插槽 -->
    <el-form label-width="120px" :model="form" :rules="rules" ref="addForm">
      <el-form-item label="部门名称" prop="name">
        <el-input v-model="form.name" style="width:80%" placeholder="1-50个字符" />
      </el-form-item>
      <el-form-item label="部门编码" prop="code">
        <el-input v-model="form.code" style="width:80%" placeholder="1-50个字符" />
      </el-form-item>
      <el-form-item label="部门负责人" prop="manager">
        <el-select style="width:80%" placeholder="请选择" />
      </el-form-item>
      <el-form-item label="部门介绍" prop="introduce">
        <el-input v-model="form.introduce" style="width:80%" placeholder="1-300个字符" type="textarea" :rows="3" />
      </el-form-item>
    </el-form>
    <!-- el-dialog有专门放置底部操作栏的 插槽  具名插槽 -->
    <div slot="footer">
      <el-button type="primary" size="small">确定</el-button>
      <el-button size="small">取消</el-button>
    </div>
  </el-dialog>
</template>

# 配置校验规则

  • 定义校验规则
rules: {
  name: [
    { required: true, message: '部门名称不能为空', trigger: ['blur', 'change'] },
    { min: 1, max: 50, message: '部门名称要求1-50个字符', trigger: ['blur', 'change'] }
  ],
  code: [
    { required: true, message: '部门编码不能为空', trigger: ['blur', 'change'] },
    { min: 1, max: 50, message: '部门编码要求1-50个字符', trigger: ['blur', 'change'] }
  ],
  manager: [
    { required: true, message: '部门负责人不能为空', trigger: ['blur', 'change'] }
  ],
  introduce: [
    { required: true, message: '部门介绍不能为空', trigger: ['blur', 'change'] },
    { min: 1, max: 300, message: '部门介绍要求1-300个字符', trigger: ['blur', 'change'] }
  ]
}

总结:

表单验证基本流程

  1. el-form绑定model/rules/ref
  2. el-form-item绑定prop
  3. 表单输入域双向数据绑定

# 部门名称和部门编码的[自定义校验]

注意:部门名称和部门编码的规则 有两条我们需要通过**自定义校验函数validator**来实现,

且判断部门名称和编码是否存在, 需要遍历, 推荐将来校验用 blur, 失去焦点校验一次即可

  • 部门名称(name):同级部门中禁止出现重复部门
  • 部门编码(code):部门编码在整个模块中都不允许重复
  1. 保存原始部门列表数据
// 获取原始的组织架构列表
async loadDepartments () {
  try {
    const ret = await reqGetDepartments()
    this.company.name = ret.data.companyName
    // 把列表数据转换为树形数据
+   this.list = JSON.parse(JSON.stringify(ret.data.depts))
    this.departs = this.translateListToTreeData(ret.data.depts, '')
  } catch {
    this.$message.error('获取组织架构数据失败')
  }
}
// data中添加一行数据
// 原始的组织列表数据
+ list: [],
  1. 把原始部门列表数据传递给add-dept.vue组件
<add-dept :list="list" :node-data="currentDept" :showDialog.sync="showDialog" />
  1. 子组件接收原始列表数据
props: {
  showDialog: {
    type: Boolean,
    required: true
  },
  // 当前部门信息
  nodeData: {
    type: Object,
    required: true
  },
  // 原始部门列表数据
+  list: {
+    type: Array,
+    required: true
+  }
},
  1. 提供自定义校验函数
data () {
  // 验证部门名称
  const checkNameRepeat = (rule, value, callback) => {
      // 查询当前部门的子部门
      const children = this.list.filter(item => {
        return item.pid === this.nodeData.id
      })
      // 判断children子部门列表中是否包含输入的部门名称value
      const flag = children.some(item => {
        return item.name === value
      })
      if (flag) {
        // 部门名称重复了
        callback(new Error('部门名称重复!'))
      } else {
        callback()
      }
  }
  // 验证部门编码
  const checkCodeRepeat = (rule, value, callback) => {
      const flag = this.list.some(item => {
        return item.code === value
      })
      if (flag) {
        // 有部门编码重复
        callback(new Error('部门编码重复'))
      } else {
        callback()
      }
  }
  return {
    ...
    rules: {
      name: [
        { required: true, message: '部门名称不能为空', trigger: ['blur', 'change'] },
        { min: 1, max: 50, message: '部门名称要求1-50个字符', trigger: ['blur', 'change'] },
        { validator: checkNameRepeat, trigger: 'blur' }
      ],
      code: [
        { required: true, message: '部门编码不能为空', trigger: ['blur', 'change'] },
        { min: 1, max: 50, message: '部门编码要求1-50个字符', trigger: ['blur', 'change'] },
        { validator: checkCodeRepeat, trigger: 'blur' }
      ],
    }
  }
}

总结:

  1. 表单的自定义验证规则
  2. 数组相关API的用法filter/some
  3. 父组件向子组件传递数据

# 处理首部内容的pid数据

这里添加根级别的部门, 需要我们手动处理下, id 需要指定为 '', 就表示是所有根级别的父节点

  • 点击首部【添加子部门】实际上添加的是一级部门(一级部门的pid应该是'')
// 公司信息
company: {
  name: '江苏传智播客教育科技股份有限公司',
  manager: '负责人',
+  id: ''
},
  • 添加弹层控制
<tree-tools @show-addbox="showAddbox" :node-data="company" :is-root="true" />

# 新增部门-部门负责人

目标:获取新增表单中的部门负责人下拉数据

在上节的表单中,部门负责人是下拉数据,我们应该从**员工接口**中获取该数据

  • 首先,封装获取简单员工列表的模块 src/api/employees.js
import request from '@/utils/request'

// 获取员工的简单列表
export function getEmployeeSimple() {
  return request({
    url: '/sys/user/simple'
  })
}
  • created中获取员工列表
import  { reqGetEmployeeSimple } from '@/api/employees'
// 负责人列表数据
elist: [],
// 触发接口调用
async handleOpen () {
  // 弹窗打开时触发
  try {
    const ret = await getEmployeeSimple()
    this.elist = ret.data
  } catch {
    this.$message.error('获取员工信息失败')
  }
},
  • 循环渲染
<el-form-item label="部门负责人" prop="manager">
  <el-select v-model="form.manager" style="width:80%" placeholder="请选择">
    <el-option v-for="item in elist" :key="item.id" :value="item.username" :label="item.username" />
  </el-select>
</el-form-item>

总结:调接口;获取数据;填充列表

注意:调用接口的时机(打开弹窗时调用)

# 如何修改提交后的备注

  1. 执行命令git commit --amend
  2. 打开vim编辑器窗口之后,按一次字符a,进入编辑状态(左下角出现【插入】)
  3. 移动光标,修改黄色的备注信息(井号开始的内容是注释,不需要处理)
  4. 修改完成后按【Esc】按键,退出编辑模式
  5. 在英文输入模式下,按 【shift + :】组合键,左下角出现光标
  6. 输入wq 保存并退出

# 回顾

  • 拆解树形组件(树的单个节点 tree-tools.vue)
  • 配置本地后台接口环境
    • 启动后端接口项目:在项目的根路径(people)执行 npm run serve
  • 获取组件架构的接口数据
  • 把列表数据转换为树形结构的数据(递归算法)
  • 删除部门(原生事件绑定.native;Element组件的事件绑定)
  • 添加部门
    • 控制弹窗的显示
    • 控制弹窗的隐藏
    • 属性绑定和事件绑定的用法可以简化为sync修饰符
    • 记录当前添加的是那个部门的子部门
  • 添加部门的表单验证
    • 基本的表单验证(基于ElementUI)
    • 验证部门名称和部门编号(自定义表单验证规则;如何获取同级部门信息)
  • 添加一级部门的弹窗控制
  • 负责人的列表数据的动态渲染

# 新增部门-提交表单

# 校验通过调用新增接口

当点击新增页面的确定按钮时,我们需要完成对表单的整体校验,如果校验成功,进行提交

  • 首先,在点击确定时,校验表单
<el-form ref="addForm" label-width="120px" :model="form" :rules="rules">
<el-button @click='handleSubmit' type="primary" size="small">确定</el-button>
// 提交表单
handleSubmit() {
  this.$refs.addForm.validate(valid => {
    if (valid) {
      // 表示可以提交了
    }
  })
}

总结:需要手动验证表单,因为可能会直接点击提交按钮

validate方法是在哪里定义的?el-form组件中定义的

# 封装新增接口

  • 首先, 封装新增部门的api模块 src/api/departments.js
export function reqAddDepartments(data) {
  return request({
    url: '/company/department',
    method: 'post',
    data
  })
}

因为是添加子部门,所以我们需要将新增的部门pid设置成当前部门的id,新增的部门就成了自己的子部门

同样,在新增成功之后,调用告诉父组件,重新拉取数据

handleSubmit () {
  this.$refs.addForm.validate(async valid => {
    if (valid) {
      try {
        // 表单验证通过
        await reqAddDepartments({
          ...this.form,
          // 父级部门的id
          pid: this.nodeData.id
        })
        // 关闭弹窗、刷新列表
        this.handleClose()
        this.$emit('on-success')
      } catch {
        this.$message.error('添加部门失败')
      }
    }
  })
},
  • 父组件
<add-dept @on-success="loadDepartments" :list="list" :node-data="currentDept" :show-dialog.sync="showDialog" />

总结:

  1. 手动表单验证
  2. 添加部门需要制定父级部门
  3. 子组件向父组件传值

# 取消按钮操作

功能:1、清空表单;2、关闭弹窗

handleClose () {
  // 控制关闭碳层
  this.$refs.addForm.resetFields()
  // this.$emit('hide-addbox')
  this.$emit('update:showDialog', false)
}
<el-button @click='handleClose' size="small">取消</el-button>

总结:重置表单的简单用法

# 编辑部门

# 展示编辑部门弹窗

编辑部门功能实际上 可以和 新增窗体采用同一个组件,只不过我们需要将新增场景变成编辑场景

  • 首先点击编辑部门时,子传父 tree-tools.vue
if (type === 'add') {
  // 添加部门
  this.$emit('show-addbox', {
    ...this.nodeData,
    type
  })
} else if (type === 'edit') {
  // 编辑部门
  this.$emit('show-addbox', {
    ...this.nodeData,
    type
  })
}

# 修改表单标题

  • 通过计算属性动态处理表单的标题
computed: {
  title () {
    return this.nodeData.type === 'add'? '新增部门': '编辑部门'
  }
},
  • 标题进行动态绑定
<el-dialog :title="title" 

总结:需要通过父组件传递的type区分编辑的添加的状态

# 获取部门数据并回填表单

  • 封装获取部门信息的模块 src/api/departments.js
// 获取部门详情数据
export function reqGetDepartDetail(id) {
  return request({
    url: `/company/department/${id}`
  })
}
  • 回调表单
async handleOpen () {
  // 弹窗打开时触发
  try {
    // 判断是否为编辑状态
    if (this.nodeData.type === 'edit') {
      // 调用接口获取部门详情数据
      const ret = await reqGetDepartDetail(this.nodeData.id)
      // 把获取到的部门详情数据填充到表单
      this.form = ret.data
    }
    const ret = await getEmployeeSimple()
    this.elist = ret.data
  } catch {
    this.$message.error('获取员工信息失败')
  }
},

总结:调用接口;获取数据;填充表单

注意:编辑时,需要使用this.nodeData.id (当前部门的id)

# 编辑提交

接下来,需要在点击确定时,同时支持新增部门编辑部门 两个场景,可以根据 form是否有id进行区分

  • 封装编辑部门接口 src/api/departments.js
// 编辑部门
export function reqUpdateDepartments(data) {
  return request({
    url: `/company/department/${data.id}`,
    method: 'put',
    data
  })
}
  • 点击确定时,进行场景区分
// 提交表单
handleSubmit () {
  this.$refs.addForm.validate(async valid => {
    if (valid) {
      if (this.nodeData.type === 'add') {
        // 添加部门
        try {
          // 表单验证通过
          await reqAddDepartments({
            ...this.form,
            // 父级部门的id
            pid: this.nodeData.id
          })
        } catch {
          this.$message.error('添加部门失败')
        }
      } else if (this.nodeData.type === 'edit') {
        // 编辑部门
        try {
          await reqUpdateDepartments(this.form)
        } catch {
          this.$message.error('编辑部门失败')
        }
      }
      // 关闭弹窗、刷新列表
      this.handleClose()
      this.$emit('on-success')
    }
  })
},

总结:如果组件进行重用,那么逻辑判断会变得复杂 如果表单非常复杂,不建议编辑和添加功能重用表单(重用后组件本身的逻辑会变复杂,后期维护就麻烦了):单一职责(一个组件就做一件事情)

# 编辑表单验证

除此之外,我们发现原来的校验规则实际和编辑部门有些冲突,所以需要进一步处理

  • 部门名称的重复验证(编辑的时候,需要验证同级部门名称是否重复;编辑的当前部门名称可以重复,但是当前部门名称不可以和同级部门名称重复)
  • 部门编号的重复验证(编辑的时候,需要验证全局是否有编号重复,编辑的当前部门的编号可以重复,但是当前部门的编号不可以和全局其他部门的编号重复)
// 验证部门名称
const checkNameRepeat = (rule, value, callback) => {
  // 添加部门时,判断子部门名称是否重复
  // 编辑部门时,判断兄弟部门是否重复
  let children = []
  if (this.nodeData.type === 'edit') {
    // 编辑部门(过滤出来的列表包含自己)
    children = this.list.filter(item => {
      // 排除当前的部门
      return item.pid === this.nodeData.pid && item.id !== this.nodeData.id
    })
  } else {
    // 添加部门
    children = this.list.filter(item => {
      return item.pid === this.nodeData.id
    })
  }
  // 判断children子部门列表中是否包含输入的部门名称value
  const flag = children.some(item => {
    return item.name === value
  })
  if (flag) {
    callback(new Error('部门名称重复!'))
  } else {
    callback()
  }
}

// 验证部门编码
const checkCodeRepeat = (rule, value, callback) => {
  const flag = this.list.some(item => {
    if (this.nodeData.type === 'add') {
      return item.code === value
    } else if (this.nodeData.type === 'edit') {
      // 如果是编辑部门,需要排除当前编辑的部门编号
      return item.code === value && item.id !== this.nodeData.id
    }
  })
  if (flag) {
    // 有部门编码重复
    callback(new Error('部门编码重复'))
  } else {
    callback()
  }
}

# 添加加载状态

由于获取数据的延迟性,为了更好的体验,可以给页面增加一个Loading进度条,采用element的指令解决方案即可

  • 定义loading变量
loading: false // 用来控制进度弹层的显示和隐藏
  • 赋值变量给指令
<div v-loading="loading" class="departments-container">
  • 获取方法前后设置变量
// 获取原始的组织架构列表
async loadDepartments () {
  try {
    this.loading = true
    const ret = await reqGetDepartments()
    this.company.name = ret.data.companyName
    // 把列表数据转换为树形数据
    this.list = JSON.parse(JSON.stringify(ret.data.depts))
    this.departs = this.translateListToTreeData(ret.data.depts, '')
  } catch {
    this.$message.error('获取组织架构数据失败')
  }
  this.loading = false
}

总结:控制加载状态ElementUI提供了一个指令 v-loading