※组织架构模块
业务功能介绍
说明:组织架构模块主要管理公司所有部门信息,支持添加、删除、编辑操作
基本组件布局
目标:使用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>
总结:
- 栅格系统的基本使用(横向有24列) el-row
- 栅格中可以再次嵌套栅格
- 栅格组件支持相关的布局(flex)
树形组件用法
目标:熟悉树形组件的基本用法
- 树形组件关键属性
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---|---|---|---|---|
| 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 配置数据的属性名称
- label 表示显示的节点名称采用的属性名称
- children 表示子节点数据的属性名称
作用域插槽
目标:熟悉作用域插槽的基本用法
- 插槽基本使用
- 具名插槽
- 作用域插槽
总结:作用域插槽用于实现:通过子组件传递给父组件插槽的数据定制子组件的模板
- 子组件把数据绑定到slot标签上即可
- 父组件通过template标签的v-slot指令获取子组件的数据,从而定制子组件的模板
实现树形的静态组织架构
目标:基于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 :data="departs" :props="defaultProps">
<!-- 中间的代码就是插槽内容,用于定制每一行的布局效果 -->
<template v-slot="scope">
<el-row type="flex" justify="space-between" align="middle" style="height: 40px; width: 100%;">
<el-col :span="20">
<span>{{ scope.data.name }}</span>
</el-col>
<el-col :span="4">
<el-row type="flex" justify="end">
<!-- 两个内容 -->
<el-col>{{ scope.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表示每一行数据的相关状态属性(比如是否展开等信息))
scope中包含了其他很多信息,如果需要可以借助调试工具查看。
- el-tree底层实现
<template>
<div>
<ul>
<li v-for="(item, index) in data" :key="index">
<!-- slot标签之前的内容是插槽的默认值 -->
<slot :data="item">{{ item.name }}</slot>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'MyTreeTest',
props: {
data: {
type: Array,
required: true
}
}
}
</script>
- 测试代码
<!-- 树形组件测试 -->
<my-tree-test :data="departs">
<template v-slot="scope">
<div>《{{ scope.data.name }}》</div>
</template>
</my-tree-test>
data () {
return {
departs: [
{ id:1, name: 'tom' },
{ id: 2, name: 'jerry' },
{ id: 3, name: 'spike' },
{ id: 4, name: 'kitty' }
],
总结:父组件把所有列表数据注入子组件,子组件再通过作用域插槽方式把其中一项数据传回父组件,从而方便父组件基于子组件传递的数据定制子组件的模板效果。
总结
- 反馈:1.获取路由映射信息;2.处理头像图片的加载失败问题(插件;自定义指令;图片的onerror事件)
- 左侧菜单结构分析
- ※template标签的使用;不可以使用v-show
- ※v-bind直接绑定对象
- ※动态组件:控制component标签的is的值,动态渲染组件获取DOM标签
- 熟悉左侧菜单的渲染流程
- ※关于左侧菜单项目的渲染
- el
- template
- render : 通过js方式渲染Vue模板,更加灵活(更加繁琐)
- JSX:在js代码中直接写HTML片段
- 插值表达式用单花括号
- 属性可以动态插值
- JSX表达式可以放到数组中
- 拆分路由模块,方便后续维护
- 静态路由和动态路由拆分,方便后续控制路由的权限(数组合并)
- 控制左侧菜单高亮:基于ElementUI实现;也可以基于路由的规则实现
- 组织架构
- 基本布局:熟悉ElementUI基本组件使用
- ※树形组件的基本使用
- ※作用域插槽的深入理解
- 基于作用域插槽定制树形组件的节点布局
拆分组织架构组件
目标: 将树形的操作内容单独抽提成组件
src/views/departments/components/TreeTools.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 v-if="!nodeData.root">编辑部分</el-dropdown-item>
<el-dropdown-item v-if="!nodeData.root">删除部门</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-col>
</el-row>
</el-col>
</el-row>
</template>
<script>
export default {
name: 'TreeTools',
props: {
nodeData: {
type: Object,
required: true
}
}
}
</script>
- 在组织架构中应用组件**
src/views/departments/index.vue**
<template>
<div class="department-container">
<el-card class="tree-card">
<!-- 用了一个行列布局 -->
<tree-tools :node-data="titleData" />
<!-- 树形菜单 -->
<el-tree :data="departs" :props="defaultProps">
<!-- 中间的代码就是插槽内容,用于定制每一行的布局效果 -->
<template v-slot="scope">
<tree-tools :node-data="scope.data" />
</template>
</el-tree>
</el-card>
</div>
</template>
- 上面代码中,company变量需要在data中定义
// 标题的数据
titleData: {
name: '江苏传智播客教育科技股份有限公司',
manager: '负责人',
root: true
},
同时,由于在两个位置都使用了该组件,但是放置在最上层的组件是不需要显示 **
删除部门和编辑部门**的所以,增加一个新的属性 **
root(是否根节点)**进行控制
export default {
props: {
// 节点的内容展示
nodeData: {
type: Object,
required: true
}
}
}
<tree-tools :node-data="company"/>
- 组件中, 根据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>
<template v-if="!nodeData.root">
<el-dropdown-item>编辑部分</el-dropdown-item>
<el-dropdown-item>删除部门</el-dropdown-item>
</template>
</el-dropdown-menu>
</el-dropdown>
总结:拆分组件为了降低代码的冗余度,也方便维护
- 封装基本的树形条目的组件(抽取动态属性)
- 导入并使用组件,然后把数据注入进去
获取组织架构接口数据
**
目标**获取真实的组织架构数据,并将其转化成树形数据显示在页面上
- 封装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.nodeData.name = ret.data.companyName
this.departs = ret.data.depts
} catch {
this.$message.error('获取组织架构数据失败')
}
}
}
总结:封装组织架构接口方法
注意:后端返回的数据是一个普通列表,但是我们需要树形结构的数据
处理数据成需要的结构
目标:转换组织架构数据我们需要将列表型的数据,转化成树形数据,这里需要用到递归算法
我们列表型数据的关键属性: id 和 pid, id指的是自己的部门id, pid指的是父级部门的id (空则没有父级部门)
- 封装工具函数
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
},
总结:从list中查找指定id值的所有子节点(递归查找)
注意:建议把该函数拆分到utils/index.js模块中,这样更加通用。
- 调用转化方法,转化树形结构
methods: {
// 获取原始的组织架构列表
async loadDepartments () {
try {
const ret = await reqGetDepartments()
this.nodeData.name = ret.data.companyName
// 把列表数据转换为树形数据
this.departs = this.translateListToTreeData(ret.data.depts, '')
} catch {
this.$message.error('获取组织架构数据失败')
}
}
}
总结:通过递归算法实现列表转换为树形结构的功能(算法)
核心流程:根据当前部门的id查询list列表中的所有子部门
删除部门
**
目标**实现操作功能的删除功能
- 封装接口删除部门
- 绑定点击按钮的事件
- 首先,封装删除功能模块
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,在el-dropdown-item组件上添加command属性
methods: {
handleAction (type) {
if (type === 'add') {
// 添加子部门
} else if (type === 'edit') {
// 编辑部门
} else if (type === 'del') {
// 删除部门
this.$confirm('确认要删除部门吗?, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
// 确认删除
const ret = await reqDelDepartments(this.nodeData.id)
// 删除成功后,刷新列表(通知父组件进行刷新)
if (ret.code === 10000) {
// 删除成功
this.$emit('on-success')
} else {
// 删除失败后提示操作
this.$message.error(ret.message)
}
}).catch(err => {
// 点击取消按钮后,err的值为cancel
if (err !== 'cancel') {
// 删除失败的错误
this.$message.error('删除部门失败!')
}
})
}
}
}
这边也可以自己注册三个点击事件, 但是自己注册需要 @click.native = "delFn"
- 用删除接口,通知父组件更新数据
删除之前,提示用户是否删除,然后调用删除接口
但是怎么通知父组件进行更新呢? 子传父, 通知父组件
this.$emit('action-success')
- 父组件监听事件
src/views/department/index.vue
<tree-tools @action-success='onSuccess' :node-data="scope.data" />
总结
- 绑定点击事件(基于Element组件进行绑定;基于原生click事件绑定的话,需要native修饰符)
- 删除需要提示
- 删除成功后需要通知父组件刷新列表:子向父传值
- 下拉列表点击事件可以基于ElementUI提供的规则处理 command
async handleAction (type) {
if (type === 'add') {
// 添加部门
} else if (type === 'edit') {
// 编辑部门
} else {
// 删除部门,确认是否要删除
const ret = await this.$confirm('确实要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).catch(err => err)
// 点击了取消操作
if (ret === 'cancel') return
// 点击确认操作
try {
const delRet = await reqDelDepartments(this.nodeData.id)
if (delRet.code === 10000) {
// 删除成功,刷新列表(子传父)
this.$emit('action-success')
} else {
// 删除失败
this.$message.error('删除部门失败')
}
} catch {
this.$message.error('删除部门失败')
}
}
}
总结:基于Async函数实现删除操作
新增部门-基本功能
新建组件-准备弹层
我们需要构建一个新增部门的窗体组件
src/views/department/components/AddDept.vue
- 准备弹窗组件
- 创建一个独立的组件
- 基于Element案例准备el-dialog组件
- 需要给基础弹窗组件提供相关的状态数据(父组件)
- 控制弹窗的显示和隐藏
- 点击添加按钮,子传父,修改弹窗标志位
- 点击弹窗的X,或者半透明的背景,触发关闭弹窗的事件 before-close
- 子传父,修改父组件的标志位
- 弹窗的基本布局
- 基本布局
src/views/departments/components/add-dept.vue
<template>
<el-dialog title="添加部门" :visible="isShowDept" :before-close="handleClose">
<span>这是一段信息</span>
<span slot="footer" class="dialog-footer">
<el-button @click="handleClose">取 消</el-button>
<el-button type="primary">确 定</el-button>
</span>
</el-dialog>
</template>
<script>
export default {
name: 'AddDept',
props: {
isShowDept: {
type: Boolean,
required: true
}
},
methods: {
handleClose () {
// 通知父组件关闭弹窗
this.$emit('on-close', false)
}
}
}
</script>
- 在**
src/views/departments/index.vue** 中引入该组件
import AddDept from './components/add-dept'
import TreeTools from './components/tree-tools'
export default {
components: {
TreeTools,
AddDept
},
}
- 定义控制窗体显示的变量**
isShowDept** src/views/departments/index.vue
<!-- 添加部门弹窗组件 -->
<add-dept :is-show-dept="isShowDept" @on-close="isShowDept=$event" />
// 父组件index.vue
data() {
return {
...
isShowDept: true
}
},
总结:
- index.vue把状态位传递给AddDept.vue组件
- 点击添加部门按钮事件发生在tree-tools.vue组件中,通知父组件打开弹窗
- 点击弹窗的关闭按钮,通知父组件关闭弹窗
注意:如果是组件上绑定的事件,event表示事件对象。
点击 x 关闭弹层 (子传父)
目标:控制添加部门组件的弹窗打开和关闭(优化)
注意:学习一个修饰符sync的用法,父子传值的双向绑定
- 子组件
add-dept.vue给 dialog 注册before-close事件
<el-dialog title="添加部门" :visible="is-show-dept" @before-close="handleClose">
- 子组件
add-dept.vue子传父, 传递 false
methods: {
handleClose() {
// 子传父
// this.$emit('on-close', false)
this.$emit('update:is-show-dept', false)
}
}
- 父组件
index.vue中, 关闭弹层
<!-- :is-show-dept="isShowDept" 父传子,不显示弹窗,当该值变成true之后,弹窗就显示了 -->
<!-- @on-close="isShowDept=$event" 子传父,关闭弹窗 -->
<!-- isShowDept 这个值在父子之间是双向传递的 -->
<!-- <AddDept :is-show-dept="isShowDept" @on-close="isShowDept=$event" /> -->
<!-- <AddDept :is-show-dept="isShowDept" @update:is-show-dept="isShowDept=$event" /> -->
<AddDept :is-show-dept.sync="isShowDept" />
// 子组件触发的事件必须采用如下格式
this.$emit('update:is-show-dept', false)
总结:
- 监听el-dialog组件的before-close事件
- 事件函数中通知父组件关闭弹窗:子传父
- 父子组件之间属性的双向绑定操作:修饰符sync(事件名称由要求:必须是【update:属性名称】)
准备弹层的结构内容
步骤
- 参考ElementUI的el-form组件
- 基于官网的案例定制表单项
<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>
总结:
- 表单基本布局(el-input需要绑定value)
- 插槽用法(具名插槽和默认插槽)
注意:报错是因为el-input组件没有绑定value值
点击[新增子部门]显示弹层组件
目标:添加部门需要知道父级部门
- 需要从tree-tools.vue组件传递给父组件,父组件记录该id
- 父组件再传递给 add-dept.vue组件
- 获取父级部门的id(add-dept.vue组件需要)
- 子组件触发新增事件·
src/views/departments/TreeTools.vue
if (type === 'add') {
// 添加部门
this.$emit('on-open', this.nodeData.id)
}
- 父组件监听事件
<tree-tools :node-data="scope.data" @on-success="onSuccess" @on-open="handleOpen" />
- 方法中弹出层,记录在哪个节点下添加子部门
data () {
return {
currentDeptId: -1
}
}
// 处理添加操作, 显示弹层
handleOpen (id) {
// 控制添加部门的弹窗显示,并且得到部门id
this.currentDeptId = id
this.isShowDept = true
},
总结
- 点击添加子部门时,需要记录当前部门的信息(id)
- 这个部门信息需要传递给AddDept.vue组件,因为表单提交数据时需要父级部门信息
总结
-
render函数基本使用规则
-
※作用域插槽:使用;封装
-
列表展示
- 树形组件如何使用
- 拆分独立的树形节点:代码复用,降低冗余度
- 拆开就设计父子组件之间的数据传递
- ※index.vaue(通过父组件让兄弟组件间接通信)
- add-dept.vue
- tree-tools.vue
- 调用接口获取部门列表数据
- 把列表转换为树(递归)
- 拆分独立的转换函数
-
删除
- 事件处理:基于ElementUI的下拉组件command事件处理
- 提示操作
- 完成删除操作
- 子传父操作
- ※async函数使用
-
添加
-
弹窗组件的基本使用
-
父子组件之间的传值
-
※sync修饰符基本使用:父子组件之间数据的双向传递
-
※$event有两层含义
- DOM事件表示事件对象
- 组件事件表示子传父的值
-
表单布局:el-form组件使用
-
获取父级部门的id
-
新增部门-表单验证
完成表单基本校验条件
表单验证条件
- 部门名称(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>
总结:基于ElementUI表单验证:
- 绑定el-form的model和rules属性
- 绑定el-form-item的prop属性
- 绑定el-input的v-model指令
配置校验规则
- 定义校验规则
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'] }
]
}
总结:
表单验证基本流程
- el-form绑定model/rules/ref
- el-form-item绑定prop
- 表单输入域双向数据绑定
- 提交表单时进行手动验证
- 添加兜底验证逻辑
// 添加部门提交表单
handleAdd () {
this.$refs.addRef.validate(valid => {
if (valid) {
// 表示验证通过,调用接口
console.log('ok')
}
})
},
部门名称和部门编码的[自定义校验]
注意:部门名称和部门编码的规则 有两条我们需要通过**自定义校验函数validator**来实现,且判断部门名称和编码是否存在, 需要遍历, 推荐将来校验用
blur, 失去焦点校验一次即可
- 部门名称(name):同级部门中禁止出现重复部门
- 部门编码(code):部门编码在整个模块中都不允许重复
- 保存原始部门列表数据
// 获取原始的组织架构列表
async loadDepartments () {
try {
const ret = await reqGetDepartments()
this.company.name = ret.data.companyName
// 把列表数据转换为树形数据
+ this.dlist = ret.data.depts
this.departs = this.translateListToTreeData(ret.data.depts, '')
} catch {
this.$message.error('获取组织架构数据失败')
}
}
// data中添加一行数据
// 原始的组织列表数据
+ dlist: [],
- 把原始部门列表数据传递给add-dept.vue组件
<add-dept :dlist="dlist" :dept-id="currentDeptId" :is-show-dept.sync="isShowDept" />
- 子组件接收原始列表数据
props: {
// 控制弹窗
isShowDept: {
type: Boolean,
required: true
},
// 父级部门的id
deptId: {
type: String,
required: true
},
// 所有部门的数据
dlist: {
type: Array,
required: true
}
},
- 提供自定义校验函数
data () {
// 自定义验证规则:验证部门名称
const validateName = (rule, value, callback) => {
// 同级部门的名称不可以重复
// 1、找到同级部门信息:根据父级部门的id
const subDepts = this.dlist.filter(item => {
return item.pid === this.deptId
})
// 2、判断输入的部门名称和同级部门名称是否冲突
const flag = subDepts.some(item => {
return item.name === value
})
// 判断存在性
if (flag) {
// 重复了
callback(new Error('同级部门名称不可以重复'))
} else {
// 不重复
callback()
}
}
// 自定义验证规则:验证部门编码
const validateCode = (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: validateName, trigger: 'blur' }
],
code: [
{ required: true, message: '部门编码不能为空', trigger: ['blur', 'change'] },
{ min: 1, max: 50, message: '部门编码要求1-50个字符', trigger: ['blur', 'change'] },
{ validator: validateCode, trigger: 'blur' }
],
}
}
}
总结:
- 表单的自定义验证规则 validator
- 数组相关API的用法filter/some/every
- 父组件向子组件传递数据
处理首部内容的pid数据
这里添加根级别的部门, 需要我们手动处理下, id 需要指定为 '', 就表示是所有根级别的父节点
- 点击首部【添加子部门】实际上添加的是一级部门(一级部门的pid应该是'')
// 公司信息
titleData: {
name: '江苏传智播客教育科技股份有限公司',
manager: '负责人',
root: true,
+ id: ''
},
- 添加弹层控制
<TreeTools :node-data="titleData" @on-open="handleOpen" />
总结:点击标题中的【添加子部门】按钮,应该传递一级部门的父id,他的值为 ‘’
新增部门-部门负责人
目标:获取新增表单中的部门负责人下拉数据
- 获取员工列表数据
- 渲染数据到下拉列表中
在上节的表单中,部门负责人是下拉数据,我们应该从**
员工接口**中获取该数据
- 首先,封装获取简单员工列表的模块
src/api/employees.js
import request from '@/utils/request'
// 获取员工的简单列表
export function getEmployeeSimple() {
return request({
url: '/sys/user/simple'
})
}
- created中获取员工列表
import { reqGetEmployeeSimple } from '@/api/employees'
// 负责人列表数据
mlist: [],
// 获取员工列表数据
async loadManagerList () {
try {
const ret = await getEmployeeSimple()
if (ret.code === 10000) {
this.mlist = ret.data
}
} catch {
this.$message.error('获取负责人列表失败')
}
},
- 触发时机:弹窗打开
<el-dialog title="添加部门" :visible="isShowDept" :before-close="handleClose" @open="loadManagerList">
- 循环渲染
<el-form-item label="部门负责人" prop="manager">
<el-select v-model="form.manager" style="width:80%" placeholder="请选择">
<el-option label="请选择..." value="" />
<el-option v-for="item in elist" :key="item.id" :value="item.username" :label="item.username" />
</el-select>
</el-form-item>
注意:调用接口的时机(打开弹窗时调用)
新增部门-提交表单
校验通过调用新增接口
当点击新增部门的确定按钮时,我们需要完成对表单的整体校验,如果校验成功,进行提交
ref 操作元素
- 操作DOM元素
- 操作组件实例
- 首先,在点击确定时,校验表单
<el-form ref="addForm" label-width="120px" :model="form" :rules="rules">
<el-button @click='handleAdd' type="primary" size="small">确定</el-button>
// 提交表单
handleAdd () {
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,新增的部门就成了自己的子部门
同样,在新增成功之后,调用告诉父组件,重新拉取数据
handleAdd () {
this.$refs.addRef.validate(async valid => {
if (valid) {
// 表示验证通过,调用接口
const ret = await reqAddDepartments({
...this.form,
pid: this.currentDeptId
})
if (ret.code === 10000) {
// 添加部门成功,刷新列表
this.$emit('action-success')
// 点击确定按钮不会自动触发before-close事件
this.handleClose()
}
}
})
},
- 父组件
<AddDept :dlist="dlist" :current-dept-id="currentDeptId" :is-show-dept.sync="isShowDept" @action-success="loadDepartList" />
总结:
- 手动表单验证
- 添加部门需要指定父级部门pid
- 子组件向父组件传值
注意:负责人表单信息需要提交username,而不是id(接口文档规定)
注意:点击确定按钮不会自动触发before-close事件
取消按钮操作
功能:1、清空表单;2、关闭弹窗
handleClose () {
// 控制关闭碳层
this.$refs.addRef.resetFields()
this.$emit('update:showDialog', false)
}
<el-button @click='handleClose' size="small">取消</el-button>
<el-dialog title="添加部门" :visible="isShowDept" :before-close="handleClose" @open="loadManagerList">
总结:重置表单的简单用法,基于ElementUI的el-form组件的实例方法resetFields实现。
注意:ref可以直接操作组件实例对象
编辑部门
展示编辑部门弹窗
编辑部门功能实际上 可以和 新增窗体采用同一个组件,只不过我们需要将新增场景变成编辑场景
- 首先点击编辑部门时,子传父
tree-tools.vue
// 处理下拉菜单的操作
handleAction (type) {
if (type === 'add') {
// 添加子部门;显示弹窗(传递当前点击的部门的id)
this.$emit('on-open', {
id: this.nodeData.id,
type: 'add'
})
} else if (type === 'edit') {
// 编辑子部门:显示弹窗,调用接口获取部门详细信息,填充表单
this.$emit('on-open', {
id: this.nodeData.id,
type: 'edit'
})
} else if (type === 'del') {
// 删除子部门
this.handleDelete(this.nodeData.id)
}
},
- 父组件监听事件
<TreeTools :node-data="titleData" @on-open="handleOpne" />
<AddDept :type="type" :dlist="dlist" :current-dept-id="currentDeptId" :is-show-dept.sync="isShowDept" @action-success="loadDepartList" />
// 控制弹窗的打开操作
handleOpen (info) {
// 当前添加子部门的父级部门的id
this.currentDeptId = info.id
// 表示操作类型:添加或者编辑
this.type = info.type
// 打开弹窗
this.isShowDept = true
},
- 通过计算属性区分弹窗的标题
computed: {
title () {
return this.type === 'add' ? '添加部门' : '编辑部门'
}
},
<el-dialog :title="title" :visible="isShowDept" @open="loadPersonList" before-close="handleClose">
总结:点击编辑按钮弹窗:添加或者编辑
- 子向父组件传值;父传子操作
- ElementUI弹窗组件基本使用
- 区分添加和编辑的标志位
获取部门数据并回填表单
目标:打开弹窗时调用接口获取部门数据,回填表单
- 封装获取部门信息的模块
src/api/departments.js
// 获取部门详情数据
export function reqGetDepartDetail(id) {
return request({
url: `/company/department/${id}`
})
}
- 回调表单
// 弹窗打开时触发方法
handleOpen () {
// 编辑时需要获取部门详情数据
if (this.operateFlag === 'edit') {
this.loadDeptInfo()
}
// 获取管理员列表数据
this.loadPersonList()
},
// 根据id查询部门的详细数据
async loadDeptInfo () {
const ret = await reqGetDepartDetail(this.deptId)
if (ret.code === 10000) {
// 获取部门信息成功
this.form.id = ret.data.id
this.form.name = ret.data.name
this.form.code = ret.data.code
this.form.manager = ret.data.manager
this.form.introduce = ret.data.introduce
}
},
总结:调用接口(弹窗打开时触发接口调用);获取数据;填充表单
注意:编辑时,需要使用this.currentDeptId(当前部门的id)
编辑表单验证
除此之外,我们发现原来的校验规则实际和编辑部门有些冲突,所以需要进一步处理
- 部门名称的重复验证(编辑的时候,需要验证同级部门名称是否重复;编辑的当前部门名称可以重复,但是当前部门名称不可以和同级部门名称重复)
- 部门编号的重复验证(编辑的时候,需要验证全局是否有编号重复,编辑的当前部门的编号可以重复,但是当前部门的编号不可以和全局其他部门的编号重复)
// 验证部门名称
const checkName = (rule, value, callback) => {
// 获取所有的同级部门(pid相同的部门就是同级部门)
const departs = this.dlist.filter(item => {
if (this.operateFlag === 'add') {
// 添加部门验证名称
return item.pid === this.deptId
} else {
// 编辑部门验证名称(此时的this.deptId是当前编辑部门的id)
// 父级部门id一样的部门是同级部门
return item.pid === this.form.pid && item.id !== this.form.id
}
})
// 判断同级部门的名称是否重复
const isOk = departs.some(item => item.name === value)
if (isOk) {
callback(new Error('同级部门不可以重名'))
} else {
callback()
}
}
// 验证部门编码
// 验证部门的编码(如果是编辑操作,那么部门编码如果不修改也可以通过验证)
const checkCode = (rule, value, callback) => {
// 所有部门都不可以有重复的编码
const isOk = this.dlist.some(item => {
if (this.operateFlag === 'add') {
return item.code === value
} else {
// 编辑的验证操作:当前编辑的部门编号可以重复(可以不修改)
// console.log(item.code, value, item.id, this.form.id)
return item.code === value && item.id !== this.form.id
}
})
if (isOk) {
callback(new Error('部门编码有重复'))
} else {
callback()
}
}
总结:
- 验证编辑的部门名称时,需要注意:部门名称可以不做修改,验证要保证通过
- 验证部门编码时要保证全局不重复,但是编辑的部门编码可以不修改,验证要保证通过
编辑提交
接下来,需要在点击确定时,同时支持
新增部门和编辑部门两个场景,可以根据form是否有id进行区分
- 封装编辑部门接口
src/api/departments.js
// 编辑部门
export function reqUpdateDepartments(data) {
return request({
url: `/company/department/${data.id}`,
method: 'put',
data
})
}
- 点击确定时,进行场景区分
// 编辑部门提交表单
async editDept () {
try {
const ret = await reqUpdateDepartments(this.form)
if (ret.code === 10000) {
// 添加成功,关闭弹窗,清空表单,刷新列表
this.handleClose()
this.$refs.addRef.resetFields()
this.$emit('on-success')
}
} catch {
this.$message.error('编辑部门失败')
}
},
// 添加部门提交表单
handleAdd () {
// 手动表单验证
this.$refs.addRef.validate(valid => {
if (!valid) return
if (this.operateFlag === 'add') {
// 添加部门表单提交
this.addDept()
} else {
// 编辑部门表单提交
this.editDept()
}
})
},
总结:调用接口提交表单,关闭弹窗,刷新列表
注意:提交的动作:添加和编辑重用事件处理逻辑
添加loading效果
由于获取数据的延迟性,为了更好的体验,可以给页面增加一个Loading进度条,采用element的指令解决方案即可
- 定义loading变量
loading: false // 用来控制进度弹层的显示和隐藏
- 赋值变量给指令
<div v-loading="loading" class="departments-container">
- 获取方法前后设置变量
// 加载部门列表数据
async loadDepartmentsList () {
try {
this.loading = true
const ret = await reqGetDepartments()
if (ret.code === 10000) {
// 把后端获取到的原始的部门列表数据传递给【转换函数】,返回最终的树形结构的数据
this.dlist = ret.data.depts
this.departs = this.translateListToTreeData(ret.data.depts, '')
this.titleData.name = ret.data.companyName
}
} catch {
this.$message.error('获取部门列表数据失败')
} finally {
this.loading = false
}
}
总结:控制加载状态ElementUI提供了一个指令 v-loading
注意:finally是原生js的结构:无论是否有异常,最终都会触发finally
总结
-
添加部门
- 基于ElementUI基本的表单验证
- 兜底验证:基于ref操作组件实例
- 自定义验证规则
- 部门名称:同级不可以重复
- 部门编码:全局不能重复
- 基于标志位控制弹窗的标题:计算属性
- 部门负责人下拉列表:调用接口;渲染下拉列表
- 添加表单提交
- 重置表单:基于ElementUI的实例方法 resetFields()
-
编辑部门
- 打开弹窗:子传父;父传子
- 回填表单数据
- ※编辑部门时
- 验证部门名称:排除自己;同级部门计算方式有变化
- 验证部门编码:排除自己
- 编辑提交表单:重用提交的业务逻辑
- 添加列表加载的loading效果
-
熟悉角色管理的基本业务
注意:index.vue父组件
- tree-tools.vue子组件
- add-dept.vue子组件