背景
- 楼主负责的项目中,存在各种选人桥,例如:组织架构桥、本部门桥和常用选人桥。在此之前,这三个桥相安无事,各有用途。
- 直到有一天,搞事的客户提了一个需求,把这个三个桥放到一起,可以一起选人。产品还给它起了个新名字叫做:聚合选人桥。乀(ˉεˉ 乀),这时候 活 就来了。淦 活
分析
聚合选人桥的结构分析
选人桥属于高频使用场景,主要承载选人的功能。
桥的页面可拆分为 人员树 页面和 已选人池 页面。
可根据桥的页面,对逻辑进行拆分。
下文中对 常用人员树 这一类嵌入的树,简称为 嵌入树
1. 人员树
1.1. 单选时,没有勾选状态
1.2. 多选时,有正反选、勾选状态的逻辑
1.3. 选人桥支持搜索人员
2. 已选人池
2.1. 包含增加和删除人员的逻辑
2.2. 多选时,删除操作需联动人员树的勾选状态
- 包含关系拆分
- 页面结构拆分(已完成效果)
面临的问题
- 旧问题:三个桥分别是已经离职的三个前辈写的,代码的结构和实现的思路不一致。想简单的把代码复用,那是不可能。多选时,选人的逻辑和选中转态的逻辑强耦合,选了人,才触发选中逻辑。
- 新特性:聚合选人桥,多选支持正反全选;已选人池的人员数据需要联动三个桥的人员树的选中状态。
思考
- 针对旧问题:
将三个桥的代码,整合到一个新组件来实现?可行,但是三个桥的代码整合的工作,不亚于重写这三个桥,工作量大,耗时长
对三个桥的代码进行差异对比,差异最大的那个桥进行重构,然后将三个桥以子组件的方式嵌入到新的组件?可行,减少需要重构的桥的数量,耗时少,工作量也减少 - 针对新特性:三个桥需要提供统一的数据入口和出口,便于勾选状态的管理。
解决方案
- 解决旧问题:通读三个桥的代码,把差异最大的一个桥,进行代码重构。 三个桥的提供一致的数据出口和入口,简化数据处理逻辑
- 新特性的实现: 把选人和已选人触发勾选状态的逻辑解耦 ,触发勾选状态支持外部数据进行触发
谈谈具体的实现
// 1.1. 处理多个页签,采取项目中 element-ui框架的 el-tabs 实现
<template>
...
<el-tabs
v-model="activeTab"
v-show="!inputting"
@tab-click="handleTabClick"
class="small-tab">
<el-tab-pane
v-for="tab in tabs"
:key="tab.id"
:name="tab.id"
:label="tab.label"
:lazy="true">
</el-tab-pane>
</el-tabs>
...
<template>
<script>
methods () {
// 在此处处理每一棵 嵌入树 的数据,达到懒加载的目的
handleTabClick () {}
}
</script>
// 1.2. 改造每一棵 嵌入树,抛出 select 和 unselect 两个事件,事件携带数据,并由他们的最上层父组件 进行统一处理。
<template>
<common-person-tree
v-for="(tree, index) in commonPersonTree"
:key="index"
:personList="selectedPerson"
:tree="tree"
:type="type"
@select="handleSelect"
@unselect="handleUnselect">
</common-person-tree>
<template>
<script>
methods () {
// 加人
handleSelect () {},
// 减人
handleUnselect () {}
}
</script>
/**
* 1.3. 嵌入树 需要提供的能力
*
* 关键点:把 选人的逻辑 和 勾选状态的逻辑 拆分。
*
* 处理选人的逻辑时,只往父组件抛事件和传数据,由选人池进行处理。
*
* 处理状态勾选时,根据选人池的传入数据和当前组的数据,单独维护状态,而不是耦合在选人的逻辑之中。
*/
<script>
data () {
countNum: 0, // 维护一个计数器,记录是否达到全选状态
isLoaded: false, // 避免二次加载
personsData: [], // 当前组的人员数据
isExpanded: false, // 是否展开
allChecked: false // 是否全选
},
props: {
tree: {
type: Object,
default: () => ({})
},
personList: {
type: Array,
default: () => ([])
}
},
watch: {
// 观察 选人池传入的 personList 再次计算 人员的选中的状态
personList (val) {
this.personsData = this.initCheckedState(val, this.personsData)
},
// 观察 计数器,判断已选人的数量 是否达到了全选的状态
countNum (val) {
this.allChecked = val === this.tree.personCount
}
},
methods () {
/**
* 1、单多选
* 2、多选支持正反选
* 3、单选无反选
*/
selectOnePerson (person) {},
/**
* 多选的 全选
* 支持正反选
*/
selectAllPerson () {},
// 首次加载人员
loadGroupPerson (groupId) {
if (this.isLoaded) return
this.fetchComPersGroupPers(groupId).then((res) => {
this.isLoaded = true
this.personsData = this.initCheckedState(this.personList, res)
})
},
/**
* 维护一个处理勾选状态的方法
* 计算人员的选中状态
*/
initCheckedState (personList, value) {
this.countNum = 0
let oIds = personList.map(item => item.oId)
value.forEach(each => {
if (includes(oIds, each.oId)) {
this.countNum++
this.$set(each, 'checked', true)
} else {
this.$set(each, 'checked', false)
}
})
return value
}
}
</script>
/*
* 2. 选人池
* 2.1 只有选人池才可以对人员的增删进行处理,其他的地方一律不能处理
* 2.2 维护一个 vuex 对选人池的数据进行管理,具体可分为:
*/
<script>
computed: {
...mapGetters({
selectedPerson: "getSelectedPerson", // 选人池 的 总数据
})
},
methods: {...mapActions([
"addSelectedPerson", // 添加一个人员
"addManySelectedPerson", // 添加几个人员 (传数组)
"delSelectedPerson", // 删除一个人员
"initSelectedPerson", // 初始化 选人池 的 总数据
"clearSelectedPerson", // 清除所有人
])}
</script>
总结
-
关键:改变了数据的流向
-
嵌入树:需要有处理自身选中状态的能力,还需要对数据传输进行改造
-
数据只能在 选人池 进行处理
-
要实现代码的可拓展,必须清晰地划分各个模块组件该做的事情。
-
最近楼主一直在看算法,这趟活干下来,觉得很适合用动态规划的思想来简单总结一下。
聚合选人桥这个大问题拆分为多个子问题,而这几个子问题又有相互联系,解决了各个子问题,也就解决了大问题。
也可以称之简单的模块化吧!