共计 11288 个字符,预计需要花费 29 分钟才能阅读完成。
介绍
因工作中须要实现一个不限层级的目录展现以及勾选问题,而支付宝小程序没有相干框架可用,故整顿一套计划解决该类问题。
该篇文章次要是记录树结构数据的解决办法,当然也可用于 web 端,思路都是一样的。内容包含一维数组转化为树结构数据,树结构数据的渲染,父子节点的联动抉择,历史节点的默认勾选等。
上面是针对关键性代码的一些解析。
源码
简略粗犷,间接上源码。
解决数据的类办法
/**
* 图片空间目录构造排序
*/
const REG_CHAR_NUM = /[a-zA-Z0-9]/ // 字母和数字正则
class CatalogSort {
/**
* @param {Object} resData 从接口或缓存拿到的数据
* @param {Boolean} isSingleChecked 是否单选 true- 单选 false- 多选
* @param {Array} historyChecked 初始化时须要选中的 ids(历史 ids)
*/
constructor(resData, isSingleChecked, historyChecked = []) {
this.treeMap = null // 父 - 子 id 映射
this.tree = null // 结构实现的树结构数据
this.currentChecked = false // 以后点击选中状态,缩小子节点递归次数
this.isSingleChecked = isSingleChecked
this.checkedList = historyChecked[0] === 'all' ? [] : [...historyChecked] // 选中目录 id 的数组
this.checkedNameList = [] // 选中的目录名称数组
this.checkedAll = historyChecked[0] === 'all' // 是否全选, 只在初始加载时用到
this.totalCount = 0 // 目录总数
this.sortInit(resData)
}
sortInit(resData) {this.solveToTree(resData)
this.sortByNumLetter(this.tree)
}
/**
* 一维数据结构树结构
* @param {Array} resData 从接口获取到的源数据
*/
solveToTree(resData) {
// 构建父 - 子映射
this.treeMap = resData.reduce((acc, cur) => {cur.pictureCategoryName = cur.pictureCategoryName.trim()
cur.children = []
if (this.checkedAll) { // 如果全选,就填充选中的 ids 数组
cur.checked = true
this.checkedList.push(cur.pictureCategoryId)
} else { // 否则依据初始化实例对象时的默认选中项设置 checked 是否选中
cur.checked = this.checkedList.includes(cur.pictureCategoryId)
}
acc[cur.pictureCategoryId] = cur
this.totalCount ++
return acc
}, {})
// 初始化选中的目录名称列表,不间接在下面循环中结构,是为了管制与 checkList 索引统一
this.checkedNameList = this.checkedList.reduce((acc, id) => {return acc.concat(this.treeMap[id].pictureCategoryName)
}, [])
// 构建树目录
this.tree = resData.filter(item => {this.treeMap[item.parentId] && this.treeMap[item.parentId].children.push(item)
return item.parentId === ''
})
}
/**
* 目录排序 按数字、字母、中文排序
* @param {Array} tree 树结构数据
*/
sortByNumLetter(tree) {tree.sort((item1, item2) => {if (REG_CHAR_NUM.test(item1.pictureCategoryName) || REG_CHAR_NUM.test(item2.pictureCategoryName)) {
// 如果是数字或字母,先依照数字或字母由 1 -10...a-z...A-Z... 排序
if (item1.pictureCategoryName > item2.pictureCategoryName) {return 1} else if (item1.pictureCategoryName < item2.pictureCategoryName) {return -1}else{return 0}
} else {// 如果是中文,依照规定排序 ( 此处依据淘宝目录设置)
return item1.pictureCategoryName.localeCompare(item2.pictureCategoryName, 'en')
}
})
tree.forEach(item => {if (item.children.length) {this.sortByNumLetter(item.children)
}
})
}
/**
* 目录抉择
*/
selectNode(id) {let item = this.treeMap[id]
item.checked = !item.checked
this.currentChecked = item.checked
if (this.isSingleChecked) {
// 如果是单选,将上一次设置的值置为 false
const checkPrev = this.checkedList.shift()
this.checkedNameList.shift()
if (checkPrev) {this.treeMap[checkPrev].checked = false
}
this.setCheckedList(item.pictureCategoryId)
return
}
this.setCheckedList(item.pictureCategoryId)
this.checkChilds(item.children, item.checked)
this.checkParents(item.parentId)
}
/**
* 目录抉择的另一种办法 (不举荐,递归了),放在这里做个借鉴,此处只兼容了多选,等须要单选再持续设置
*/
/*selectNodeAnother(tree, id) {for (let item of tree) {if (item.pictureCategoryId === id) {
item.checked = !item.checked
this.currentChecked = item.checked
if (this.isSingleChecked) {return true}
this.setCheckedList(item.pictureCategoryId)
this.checkChilds(item.children, item.checked)
this.checkParents(item.parentId)
return true
}
if (item.children.length) {const res = this.selectNode(item.children, id)
if (res) return res
}
}
return null
}*/
/**
* 子节点 checked 状态的扭转
* @param {Array} childItems 须要操作的子节点
* @param {Boolean} checked 是否选中
*/
checkChilds(childItems, checked) {if (childItems.length) {
childItems.forEach(item => {
// 如果子节点曾经是以后的选中态,跳出,缩小递归次数
if (item.checked === this.currentChecked) return
item.checked = checked
this.setCheckedList(item.pictureCategoryId)
this.checkChilds(item.children, checked)
})
}
}
/**
* 父节点 checked 状态的扭转
* @param {String} parentId 父 id
* @param {Object} treeMap 父 - 子 id 映射对象
*/
checkParents(parentId) {if (this.treeMap[parentId] && this.treeMap[parentId].children.length) {const parentChecked = this.treeMap[parentId].children.every(item => item.checked)
if (this.treeMap[parentId].checked === parentChecked) return // 如果父节点与须要选中的状态统一,则退出循环,不须要再往上冒泡递归
this.treeMap[parentId].checked = parentChecked
this.setCheckedList(this.treeMap[parentId].pictureCategoryId)
this.treeMap[parentId].parentId && this.checkParents(this.treeMap[parentId].parentId)
}
}
/**
* 设置选中 ids
* @param {String} id 正在设置 checked 属性的节点 id
*/
setCheckedList(id) {const checkedIndex = this.checkedList.findIndex(item => item === id)
if (this.currentChecked && checkedIndex === -1) { // 如果以后态选中,且节点 id 不在选中数组中就填充
this.checkedList.push(id)
this.checkedNameList.push(this.treeMap[id].pictureCategoryName)
} else if (!this.currentChecked && checkedIndex > -1) { // 如果以后态未选中,且节点 id 在选中数组中就删除
this.checkedList.splice(checkedIndex, 1)
// this.checkedNameList.findIndex(name => name === this.treeMap[id].pictureCategoryName) 不必此办法是避免重名导致删除出错
// 管制名称插入与 id 插入统一,即可间接依据独特索引来删除
this.checkedNameList.splice(checkedIndex, 1)
}
}
}
export {CatalogSort}
父组件
-
acss
/* 目录树弹窗 */ .selectCatalogDialog_contentStyle { flex: 1; width: 750rpx; background-color: rgba(0, 0, 0, 0.6); display: flex; position: fixed; top: 0; left: 0; z-index: 66; flex-direction: column; height: 100vh; } .selectCatalogDialog_Btn { height: 114rpx; background-color: #ffffff; border-radius: 24rpx; margin: 20rpx 25rpx 25rpx 25rpx; justify-content: center; display: flex; align-items: center; } .selectCatalogDialog_Btn_txt { font-size: 32rpx; color: #3089dc; text-align: center; flex: 1; line-height: 114rpx; } .comfirm-btn {border-left: 1rpx solid #ddd;} .selectCatalogDialog_body { position: relative; border-radius: 24rpx; background-color: #ffffff; margin: 25rpx 25rpx 0 25rpx; flex: 1; overflow: hidden; display: flex; } .catalog_list_row_refresh_block { position: absolute; top: 26rpx; right: -30rpx; z-index: 1; flex: 1; flex-direction: row; justify-content:flex-end; align-items: center; display: flex; } .catalog_list_row_refresh_touch { width: 150rpx; flex-direction: row; align-items: center; display: flex; } .catalog_list_row_refresh_touch_text {color: #3089dc;}
-
axml
<view class="selectCatalogDialog_contentStyle" style="left:{{show?'0rpx':'-750rpx'}}"> <scroll-view class="selectCatalogDialog_body" scroll-y="{{true}}"> <block a:if="{{cataloglist}}"> <view class="catalog_list_row_refresh_block"> <view class="catalog_list_row_refresh_touch" onTap="refreshCategory"> <image src="{{imgUrl+'/imageOff/common/refresh.png'}}" style="width:35rpx;height:35rpx"/> <view class="catalog_list_row_refresh_touch_text"> 刷新 </view> </view> </view> <tree-child a:for="{{cataloglist}}" catalog="{{item}}" level="{{0}}" onSelectCatalog="onSelectCatalog"></tree-child> </block> </scroll-view> <view class="selectCatalogDialog_Btn"> <text onTap="hide" class="selectCatalogDialog_Btn_txt"> 勾销 </text> <text a:if="{{!isSingleChecked}}" onTap="confirm" class="selectCatalogDialog_Btn_txt comfirm-btn"> 确定 </text> </view> </view>
-
js
import PictureCatalogService from '/pages/pictureSpace/public/PictureCatalogService.js'; import {RyUrlConfigure} from '../../js/together/RyUrlConfigure.js' import {CatalogSort} from '../../js/together/catalogSort.js' const app = getApp(); Component({mixins: [], data: {imgUrl: RyUrlConfigure.getUrlImg(), cataloglist: null, }, props: { show: false, isSingleChecked: false, // 是否单选 false- 多选 true- 单选 historyChecked: [], // 已选中的列表 onSelect: () => {}, onClose: () => {}, }, didMount() {this.loadData() this.treeInstance = null // 树结构实例 }, didUpdate() {}, didUnmount() {}, methods: {loadData(){PictureCatalogService.getCatalogTreeData().then((data) => {this.treeInstance = new CatalogSort(data, this.props.isSingleChecked, this.props.historyChecked) this.setData({cataloglist: this.treeInstance.tree}) }).catch(err => {my.alert({ content: err}) }); }, /** * 图片目录抉择 */ onSelectCatalog(id) {this.treeInstance.selectNode(id) this.setData({cataloglist: this.treeInstance.tree}, () => { // 单选选中后间接将选中的值传递给父组件 if (this.props.isSingleChecked) {this.props.onSelect(this.treeInstance.treeMap[this.treeInstance.checkedList[0]]) } }) }, hide(){this.props.onClose() }, /** * 多选时点击确定 */ confirm() { // 将选中的 ids 传递给父组件 const {checkedList, checkedNameList, totalCount} = this.treeInstance if (checkedList.length === 0) {app.toast('请先抉择目录') return } const values = { checkedList, checkedNameList, totalCount, } this.props.onSelect(values) }, refreshCategory(){PictureCatalogService.getCatalogTreeData(true).then(res=>{app.toast('刷新胜利') this.loadData()}).catch(err => {my.alert({ content: err}) }) } }, });
-
json
{ "component": true, "usingComponents": { "check-box": "/common/components/ryCheckbox/index", "am-checkbox": "mini-ali-ui/es/am-checkbox/index", "tree-child": "/common/components/picCategoryTree/tree-child/tree-child" } }
子组件 (树递归渲染节点)
-
acss
.catalog_list_row { border-bottom: 1rpx solid #dddddd; background-color: #ffffff; height: 89rpx; flex-direction: row; align-items: center; position: relative; display: flex; padding-left: 20rpx; } .catalog_list_row_select_block { flex: 1; flex-direction: row; align-items: center; display: flex; } .catalog_list_row_title { font-size: 28rpx; color: #333333; margin-left: 20rpx; } .child_toggle_btn { position: absolute; height: 88rpx; width: 78rpx; justify-content: center; align-items: center; right: 0rpx; top: 0rpx; display: flex; } .child_toggle_btn_icon { width: 38rpx; height: 38rpx; }
-
axml
<view style="width: 100%;"> <view class="catalog_list_row"> <view onTap="checkNode" data-id="{{catalog.pictureCategoryId}}" class="catalog_list_row_select_block" style="padding-left:{{level * 50}}rpx;"> <check-box checked="{{catalog.checked}}" /> <view class="catalog_list_row_title">{{catalog.pictureCategoryName}}</view> </view> <view onTap="toggleChild" class="child_toggle_btn" a:if="{{isBranch && catalog.pictureCategoryId !=='0'}}"> <image class="child_toggle_btn_icon" src="{{ImgUrlPrefix + (open ?'icon-fold.png':'icon-unfold.png')}}" /> </view> </view> <view hidden="{{!open}}"> <tree-child a:for="{{catalog.children}}" catalog="{{item}}" level="{{level + 1}}" onSelectCatalog="selectCatalog"></tree-child> </view> </view>
-
js
import {RyUrlConfigure} from '../../../js/together/RyUrlConfigure.js' Component({mixins: [], data: {imgUrl: RyUrlConfigure.getUrlImg(), ImgUrlPrefix: RyUrlConfigure.getUrlImg() + '/imageOff/picture-space/', open: false, // 是否开展 isBranch: false, // 是否是父节点 }, props: {catalog: {}, // 树结构对象 level: 0, // 层级 onSelectCatalog: () => {}, }, didMount() { this.setData({ isBranch: this.props.catalog.children.length > 0, open: this.props.level === 0, }) }, didUpdate() {}, didUnmount() {}, methods: {toggleChild() { this.setData({open: !this.data.open,}) }, checkNode({target: { dataset: { id} } } = e) {this.selectCatalog(id) }, selectCatalog(id) {this.props.onSelectCatalog(id) }, }, });
-
json
{ "component": true, "usingComponents": { "check-box": "/common/components/ryCheckbox/index", "tree-child": "/common/components/picCategoryTree/tree-child/tree-child" } }
步骤解析
一维数组转化为树结构
因从接口中获取到的数据不是曾经结构好了的树数据,而是一维数组,所以咱们须要先将其转化为树结构。此处内容关键点在于构建父 - 子节点映射关系,不便后续节点的查找,大大减少了递归的调用。
初始化遍历源数组,构建父 - 子关系时减少了默认的选中项,因全选的时候可能不会传所有的节点而只是传一个代表全选的字段,故此处会暂存一个初始化时是否全选的判断,以给每个子项减少 checked(是否选中)状态项,totalCount 值与勾选的数组长度值用于判断是否全选。
正式构建树结构,遍历源数组,依据每一项的父节点 id,利用后面刚结构好的父 - 子映射对象,给每一项对应的父节点填充相应的子节点项。最初利用 filter 返回第一层数据,即无父节点关系(此处为空)。目录排序
目录排序利用递归给每一层数据进行数字、字母、中文排序,localeCompare 在不同浏览器中兼容性不同,故这里先判断目录名是否为数字字母,是的话用 sort 先进行排序,否则再用 localeCompare。
树结构渲染
树渲染用到了递归组件,所以须要拆分为父子两个组件来进行渲染,子组件次要是用于渲染每一个目录单项,而后递归子组件来渲染整棵树。
父组件引入子组件时,申明一个 level 为 0 的属性示意层级,后续通过层级来指定子组件的 padding-left 值给予缩进,构建父子视觉关系。
子组件用两个属性值操作开展收起,isBrand 示意是否有子节点,open 示意是否开展,通过 level 操控默认的开展层级;开展收起状态图标通过是否有子节点的形式进行展现(即 isBrand 属性)。因我的业务中第一层为一个父节点,不收起,故第一层不渲染开展收起图标。isOpen 状态值用于判断是否暗藏子节点内容。这两个属性只针对节点自身,而不必初始化时给节点的每一项减少相似于 checked 的属性,十分不便事件操作,而不必再递归树结构数组。树节点抉择
节点抉择包含单选、多选两方面,通过初始化树组件实例传 isSingleChecked 判断是否单选,传 historyChecked 示意默认选中项。选中节点时,selectNode 办法,通过父 - 子节点映射查找到选中的节点。
单选时,存储一下上一次的选中值(用于每一次单选将其 checked 置为 false)以及本次的抉择状态(是选中还是勾销选中);同时将选中的 id 存入 checkedList 数组(此处多存了一个 name 数组,为了业务需要),存储时如果是选中且不在选中数组中就 push,如果是勾销选中且在数组中就删除 id。
能够多选时,针对父 - 子抉择的关系就更简单,除了扭转自身选中态外,也要对其子节点和父节点进行遍历抉择。父子节点递归的过程中通过判断是否与以后节点态统一来跳出循环,缩小递归次数。子节点选中间接递归 children 子数组即可;父节点抉择通过父子映射列表查找父关系,再对父关系的子节点列表的 checked 属性进行判断,若都为 true,则选中,否则为不选中状态。组件接管
组件接管值次要是节点的选中 ids 数组,选中的节点名称数组,总长度。若前期有其余业务可在此扩大。
结语
以上就是对于树结构业务的所有思路。为了不便前期查看,在此记录。若其中有能帮忙到他人的,十分开心。