Multiple Folder Icons

介绍了MFI(Multi-Folder Icons)工具,该工具允许用户轻松地为不同组的文件夹分配不同的图标,便于快速识别文件夹结构,特别是在驱动器映射或同步项目的情况下。在文件打开对话框中也能显示这些自定义图标。

原文:http://www.codeproject.com/useritems/MFI.asp

Introduction

Example of mutiple folder icons

There are two ways to change a folder icon in Windows:

  • Change the icon for a specific folder (right click an choose "Customize This Folder...")
  • Change the icon used for all folders globally

But what if you want to use different icons for a subset of folders ? Why would you want to ? Consider the following situations:

  • You have a drive mapping (or subst) that sometimes points to one set of folders (say production) and sometimes to another set (say development)
  • You have two connected machines (say your laptop and your desktop) with identical folder structure for synchronized projects
  • You want to quickly identify parts of the folder structure
  • err...
  • ...that's it.

In all the above examples the ability to select an icon for parts of the folder structure can be a real benefit. Unfortunately there is no easy way in Windows to do this (you could of course change the icon manually for each folder !).

Multi-Folder Icons (MFI) lets you do this easily - the image above shows it in operation with several distinct groups of folders shown using different icons.

Many File Open dialogs are implemented using an Explorer view so you also get the benefit of the custom icons when opening and saving files:...

<template> <view :class="{ 'next-tree': !toEject, 'next-tree-eject': toEject }" v-if="showTree"> <view v-if="toEject && needMask" :class="[{ show: showTree }, 'next-tree-mask']" @tap="_cancel(1)"></view> <view :class="[{ show: showTree }, 'next-tree-cnt']"> <view class="next-tree-bar" v-if="needBtn && componentType != 'customer'"> <view class="next-tree-bar-cancel" :style="{ color: cancelColor }" hover-class="hover-c" @tap="_cancel" >取消</view > <view class="next-tree-bar-title" :style="{ color: titleColor }">{{ title }}</view> <view class="next-tree-bar-confirm" :style="{ color: confirmColor }" hover-class="hover-c" @tap="_confirmBtn" >确定</view > </view> <view class="next-tree-bar-box" v-if="componentType == 'customer'"> <view class="next-tree-bar-customer" :style="{ color: titleColor }">{{ title || '客户所属行业' }}</view> <uni-icons class="next-tree-bar-customer-icon" type="close-outline" size="13" color="#999999" @tap="_cancel" /> </view> <slot></slot> <view class="next-tree-view" :class="{ customer: componentType == 'customer' }" :style="nextTreeStyleCp" > <scroll-view class="next-tree-view-sc" :scroll-y="true" :style="nextTreeScrollStyleCp"> <slot name="empty"></slot> <block v-for="(item, index) in treeList" :key="index"> <view class="next-tree-item" :style="[ { marginLeft: componentType != 'customer' ? item.rank * marginLeftNum + 'px' : item.rank == 0 ? '8px' : '0px', paddingLeft: componentType != 'customer' ? '0px' : (item.rank - 1) * marginLeftNum + 20 + 'px', zIndex: item.rank * -1 + 50 } ]" :class="{ border: showBorder(item.show, item.lastRank), show: item.show, last: item.lastRank, showchild: item.showChild, open: item.open, first: !item.parentIdNub }" > <view class="next-tree-label" :class="{ checked: item.checked && componentType != 'customer', disabled: item.disabled }" > <uni-icons v-if="!item.lastRank && componentType != 'customer'" class="next-tree-icon" :size="iconSize" :type="item.lastRank ? lastIcon : item.showChild ? currentIcon : defaultIcon" :color="treeIconColor" @tap.stop="_treeItemTap(item, index)" /> <uni-icons v-if="item.lastRank && zipList" class="next-tree-icon" :size="zipListIconSize" :type="imgTypeFn(item.type)" @tap.stop="_treeItemTap(item, index)" /> <uni-icons v-if="item.parentIdNub && componentType == 'customer'" class="next-tree-icon-zhexian" :size="8" color="#CCCCCC" :type="'zhexian'" @tap.stop="_treeItemSelect(item, index)" /> <view class="next-tree-icon-not-selected" v-if="!item.checked && !item.indeterminate && componentType == 'customer'" @tap.stop="_treeItemSelect(item, index)" > </view> <uni-icons v-if="componentType == 'customer' && item.checked" class="next-tree-icon" :size="iconSize" :type="'all-selected'" :color="'#0F56D5'" @tap.stop="_treeItemSelect(item, index)" /> <uni-icons v-if="componentType == 'customer' && item.indeterminate" class="next-tree-icon" :color="'#0F56D5'" :size="iconSize" :type="'indeterminate'" @tap.stop="_treeItemSelect(item, index)" /> <!-- {{indeterminate}} --> <!-- <view v-if="!item.lastRank" class="next-tree-icon">{{ item.lastRank ? '' : item.showChild ? 'X' : '>' }}</view> --> <view v-if="!zipList" @tap.stop="_treeItemSelect(item, index)"> {{ item.name }}</view> <view v-if="zipList" class="zipList_box" @tap.stop="_treeItemSelect(item, index)"> <view class="zipList_box_one">{{ item.name }}</view> <view v-if="item.lastRank" class="zipList_box_two">文件大小:{{ getSize(item.size) }}</view> </view> </view> <view class="next-tree-check" @tap.stop="_treeItemTap(item, index)"> <view class="next-tree-check-yes" v-if="item.checked && !zipList" :class="{ radio: !multiple }"> <!-- <text class="icon-text">✔</text> --> <uni-icons v-if="componentType != 'customer'" :type="checkIcon" color="#0f56d5" /> </view> <view class="next-tree-check-yes" v-if="item.lastRank && zipList" :class="{ radio: !multiple }"> <uni-icons type="forward" color="#CCCCCC" size="12" /> </view> <view class="next-tree-check-no" v-else :class="{ radio: !multiple }"></view> </view> <uni-icons v-if="!item.lastRank && componentType == 'customer'" class="next-tree-icon" :size="iconSize" :type="item.open ? 'up' : 'down'" color="#333" @tap.stop="_treeItemSelect(item, index)" /> </view> </block> </scroll-view> <view class="next-tree-view-bottom" v-if="componentType == 'customer'"> <view class="next-tree-view-bottom-cancel" hover-class="hover-c" @tap="_cancel">取消</view> <view class="next-tree-view-bottom-confirm" hover-class="hover-c" @tap="_confirmBtn">确定</view> </view> </view> </view> <view v-if="!toEject && needMask" :class="[{ show: showTree }, 'next-tree-mask-eject']" @tap="_cancel(1)"></view> </view> </template> <script> /** * zy-tree 树组件 * @description 树组件 * @property {Array} treeData 组件数据{needShowChild: true,当前项与父级展开,当前项子级不展开 needOpen: true,当前项子级与父级展开,checked: true默认选中的值,irrevocable: true不能取消选中,选中的值父级自动打开} * @property {String} valueKey 选中值键名 * @property {String} labelKey 展示值键名 * @property {String} childrenKey 子类数据键名 * @property {String} title 标题 * @property {Boolean} multiple 是否可以多选 * @property {Boolean} selectParent = 是否可以选父级 * @property {Boolean} foldAll = 折叠时关闭所有已经打开的子集,再次打开时需要一级一级打开 * @property {String} confirmColor = 确定按钮颜色 * @property {String} cancelColor = 取消按钮颜色 * @property {String} titleColor = 标题颜色 * @property {String} currentIcon = 展开时候的icon * @property {String} checkIcon = 选中时候的icon * @property {String} defaultIcon = 折叠时候的icon * @property {String} lastIcon = 没有子集的icon * @property {Boolean} border = 是否有分割线 * @property {Boolean} needBtn = 是否有操作按钮 * @property {Boolean} toEject = 是否是从底部弹出 * @property {Boolean} needMask = 是否是从底部弹出 * @property {String} boxHeight = 是否是从底部弹出时组件可选区域的高度默认112px * @property {Boolean} clickMask = 点击遮罩层关闭组件 * @property {String} treeIconColor = 父级icon颜色 * @property {String} componentType = 组件类型('customer') * @event {Function} _cancel弹窗关闭触发的方法 * @event {Function} _confirmBtn数据选中或有按钮时点击‘确定’触发的方法 * @event {Function} 可通过ref调用组件内方法_show,进行组件的展示 * @event {Function} 可通过ref调用组件内方法_hide,进行组件的关闭但不触发cancel事件 * @event {Function} 可通过ref调用组件内_reTreeList()对数据进行重置 */ export default { name: 'ZyTree', props: { treeData: { type: Array, default: function () { return [] } }, valueKey: { type: String, default: 'id' }, labelKey: { type: String, default: 'label' }, childrenKey: { type: String, default: 'children' }, treeIconColor: { type: String, default: '#0f56d5' }, title: { type: String, default: '' }, multiple: { // 是否可以多选 type: Boolean, default: true }, selectParent: { //是否可以选父级 type: Boolean, default: true }, foldAll: { //折叠时关闭所有已经打开的子集,再次打开时需要一级一级打开 type: Boolean, default: false }, confirmColor: { // 确定按钮颜色 type: String, default: '#0f56d5' }, cancelColor: { // 取消按钮颜色 type: String, default: '#0f56d5' }, titleColor: { // 标题颜色 type: String, default: '' }, currentIcon: { // 展开时候的icon type: String, default: 'folder' }, checkIcon: { // 选中时候的icon type: String, default: 'checkmarkempty' }, defaultIcon: { // 折叠时候的icon type: String, default: 'folder' }, lastIcon: { // 没有子集的icon type: String, default: '' }, border: { // 是否有分割线 type: Boolean, default: true }, faNoBorder: { // 是否有分割线 type: Boolean, default: false }, needBtn: { // 是否有操作按钮 type: Boolean, default: false }, toEject: { // 是否是从底部弹出 type: Boolean, default: false }, boxHeight: { // 是否是从底部弹出时组件可选区域的高度默认100% type: String, default: '500px' }, clickMask: { // 点击遮罩层关闭组件 type: Boolean, default: true }, openAll: { // 全部打开 type: Boolean, default: false }, irrevocableAll: { // 全部不能取消选中 type: Boolean, default: false }, nextTreeStyle: { // 样式 type: Object, default: () => ({}) }, nextTreeScrollStyle: { // 样式 type: Object, default: () => ({}) }, defaultValue: { // 默认选中的值 type: Array, default: function () { return [] } }, nextTreeMinHeight: { type: String, default: '500px' }, needClickFn: { // 选中子节点是否需要触发confirm type: Boolean, default: false }, otherValueKey: { // 额外选中的值 type: Array, default: function () { return [] } }, needMask: { // 是否需要遮罩层关闭组件 type: Boolean, default: true }, allWayShow: { // 是否一直展示 type: Boolean, default: false }, iconSize: { // icon大小默认16 type: String, default: '16' }, zipList: { // 是否是文件展示列表 type: Boolean, default: false }, zipListIconSize: { // 文件列表icon大小默认34 type: String, default: '34' }, marginLeftNum: { // 每行左边缩进的距离 type: Number, default: 25 }, componentType: { // 组件类型('customer')customer~是客户编辑时编辑的样式,正常为'' type: String, default: '' } }, watch: { defaultValue: { deep: true, handler() { this._defaultSelect() // 只更新已选中的节点,而不是重建整棵树 } } }, data() { return { showTree: false, treeList: [], selectIndex: -1, childNums: [], rt: [], rtName: [], otherValue: [], cklickType: { type: '', value: '' } } }, computed: { nextTreeStyleCp() { return { minHeight: this.nextTreeStyle.minHeight ?? this.nextTreeMinHeight, height: this.nextTreeStyle.height ?? this.boxHeight, ...this.nextTreeStyle } }, nextTreeScrollStyleCp() { return { minHeight: this.nextTreeStyle.minHeight ?? this.nextTreeMinHeight, ...this.nextTreeStyle } } }, methods: { imgTypeFn(type) { let imgType = 'oth' const typeMap = { img: ['png', 'jpg', 'jpeg', 'gif', 'bmp'], exel: ['xls', 'xlsx', 'xlsm'], word: ['doc', 'docx'], pdf: ['pdf'], ppt: ['ppt', 'pptx'], txt: ['txt'] } for (let key in typeMap) { if (typeMap[key].includes(type)) { imgType = key } } return imgType }, getSize(val) { let sizeInKB = val / 1024 return sizeInKB.toFixed(2) + 'KB' }, showBorder(show, item) { if (this.faNoBorder && !item) { return false } else { return show && this.border } }, _show() { this.showTree = true this.$emit('showTree', true) this._confirm() }, _hide() { this.$emit('showTree', false) this.showTree = false }, _cancel(val) { if (val == 1 && !this.clickMask) return this._hide() this.$emit('cancel', '') }, _confirm(val) { console.log('🚀 ~ _confirm ~ val:', val) this.rt = [] this.rtName = [] this.otherValue = [] // 处理所选数据 this.treeList.forEach(item => { // if (item.id == 100 && item.checked) { if (item.checked) { return this.confirmEach(this.treeData) } // if (item.checked && item.parentId && item.parentId != '100') { if (item.checked && item.parentId) { this.rt.push(item.id) this.rtName.push(item.name) this.otherValueKey.forEach(key => { if (!this.otherValue[key]) { this.otherValue[key] = [] // 初始化数组 } this.otherValue[key].push(item[key]) // 将对应字段值加入数组 }) } }) if (val == 1 && !this.allWayShow) { this._cancel() } this.$emit('confirm', { id: this.rt, name: this.rtName, otherValue: this.otherValue }, { ...this.cklickType }) }, _confirmBtn() { this.rt = [] this.rtName = [] this.otherValue = [] // 处理所选数据 this.treeList.forEach(item => { // if (item.id == 100 && item.checked) { // if (item.checked) { // return this.confirmEach(this.treeData) // } // if (item.checked && item.parentId && item.parentId != '100') { if (item.checked && item.parentId) { this.rt.push(item.id) this.rtName.push(item.name) this.otherValueKey.forEach(key => { if (!this.otherValue[key]) { this.otherValue[key] = [] // 初始化数组 } this.otherValue[key].push(item[key]) // 将对应字段值加入数组 }) } }) this.$emit('_confirmBtn', { id: this.rt, name: this.rtName, otherValue: this.otherValue }) }, confirmEach(list) { list.forEach(item => { if (item.isDy) { this.rtName.push(item.name) this.rt.push(item.id) this.otherValueKey.forEach(key => { if (!this.otherValue[key]) { this.otherValue[key] = [] // 初始化数组 } this.otherValue[key].push(item[key]) // 将对应字段值加入数组 }) } if (item.children && item.children.length > 0) this.confirmEach(item.children) }) }, //扁平化树结构 _renderTreeList(list = [], rank = 0, parentId = [], parents = []) { list.forEach(item => { this.treeList.push({ ...item, id: item[this.valueKey], name: item[this.labelKey], source: item, parentIdNub: item.parentId, parentId, // 父级id数组 parents, // 父级id数组 rank, // 层级 showChild: false, //子级是否显示 needShowChild: this.openAll ? this.openAll : item.needShowChild ? item.needShowChild : false, //子级是否显示 open: false, //是否打开 needOpen: this.openAll ? this.openAll : item.needOpen ? item.needOpen : false, //是否打开 show: rank === 0, // 自身是否显示 hideArr: [], orChecked: this.defaultValue.some(obj => obj == item[this.valueKey]) ? true : item.checked ? true : false, checked: this.defaultValue.some(obj => obj == item[this.valueKey]) ? true : item.checked ? true : false, disabled: item.disabled ? item.disabled : false, isParent: item[this.childrenKey] ? true : false, irrevocable: this.irrevocableAll ? this.irrevocableAll : item.irrevocable, indeterminate: false }) if (Array.isArray(item[this.childrenKey]) && item[this.childrenKey].length > 0) { let parentid = [...parentId], parentArr = [...parents] // childrenid = [] delete parentArr.children parentid.push(item[this.valueKey]) parentArr.push({ [this.valueKey]: item[this.valueKey], [this.labelKey]: item[this.labelKey] }) this._renderTreeList(item[this.childrenKey], rank + 1, parentid, parentArr) } else { this.treeList[this.treeList.length - 1].lastRank = true } }) }, _defaultSelect() { this.treeList.forEach(v => { const isChecked = this.defaultValue.some(obj => obj === v.id) if (isChecked !== v.checked) { v.checked = isChecked } }) // 同步父子级选中状态 this.treeList.forEach(v => { if (v.checked) { this._handleCheckedNode(v) } if (v.needOpen) { this._handleOpenNode(v) } if (v.needShowChild) { this._handleShowChildNode(v) } }) }, _handleCheckedNode(v) { this.treeList.forEach(v2 => { if (v.parentId.toString().indexOf(v2.parentId.toString()) >= 0) { v2.show = true if (v.parentId.includes(v2.id)) { v2.showChild = true v2.open = true } } }) }, _handleOpenNode(v) { v.showChild = true v.open = true v.show = true this.treeList.forEach(v2 => { if (v2.parentId.toString().indexOf(v.id.toString()) >= 0) { v2.show = true if (v2.parentId.includes(v.id)) { v2.showChild = true v2.open = true } } if (v.parentId.includes(v2.id)) { v2.showChild = true v2.open = true } }) }, _handleShowChildNode(v) { this.treeList.forEach(v2 => { if (v.parentId.toString().indexOf(v2.parentId.toString()) >= 0) { v2.show = true if (v.parentId.includes(v2.id)) { v2.showChild = true v.showChild = true } } if (v.id === v2.id) { v.showChild = false v.open = false } }) }, // 点击 _treeItemTap(item, index) { if (this.zipList && item.lastRank == true) { //点击最后一级时触发事件 this.treeList[index].checked = !this.treeList[index].checked this._fixMultiple(index) if (this.needClickFn) { this._confirm() } this.$emit('lastRank', item, this.treeList[index].checked) return } this.cklickType = { type: '', value: '' } if (item.disabled) return if (item.irrevocable && this.treeList[index].checked) return if (item.lastRank == true) { //点击最后一级时触发事件 this.treeList[index].checked = !this.treeList[index].checked this._fixMultiple(index) if (this.needClickFn) { this._confirm() } //子级选择/选满/取消选择, 父级往上同步状态 if (this.componentType == 'customer') { console.log(1) this.setAncestorsCustomer(item.parentId, this.treeList[index].checked, item.id) } this.$emit('lastRank', item, this.treeList[index].checked) return } let list = this.treeList let id = item.id if (item.showChild && !item.open) { item.showChild = false item.open = false } else { item.showChild = !item.showChild } item.open = item.showChild ? true : !item.open list.forEach(childItem => { if (item.showChild === false) { this.cklickType = { type: 'click', value: 'close' } //隐藏所有子级 if (!childItem.parentId.includes(id)) { return } if (!this.foldAll) { if (childItem.lastRank !== true && !childItem.open) { childItem.showChild = false } // 为隐藏的内容添加一个标记 if (childItem.show) { childItem.hideArr[item.rank] = id } } else { if (childItem.lastRank !== true) { childItem.showChild = false } } childItem.show = false } else { this.cklickType = { type: 'click', value: 'open' } // 打开子集 if (childItem.parentId[childItem.parentId.length - 1] === id) { childItem.show = true } // 打开被隐藏的子集 if (childItem.parentId.includes(id) && !this.foldAll) { if (childItem.hideArr[item.rank] === id) { childItem.show = true if (childItem.open && childItem.showChild) { childItem.showChild = true } else { childItem.showChild = false } childItem.hideArr[item.rank] = null } } } }) if (this.selectParent) { this.$emit('faClick', item, this.cklickType.value) } if (!this.selectParent) return if (!this.needBtn || this.needClickFn) { this._confirm() } }, _treeItemSelect(item, index) { this.cklickType = { type: '', value: '' } if (this.selectParent) { this.$emit('faClick', item, !this.treeList[index].checked) } if (item.lastRank == true) { this.$emit('lastRank', item, !this.treeList[index].checked) } if (item.disabled) return if (!this.selectParent && !item.lastRank) return this._treeItemTap(item, index) if (item.irrevocable && this.treeList[index].checked) return this.treeList[index].checked = !this.treeList[index].checked // 选父级, 子级自动全选 // this.syncChecked(this.treeList, item.id, this.treeList[index].checked) this.syncChecked(item.id, this.treeList[index].checked) if (item.rank > 0) { item.parentId.forEach(pid => { const parent = this.treeList.filter(i => i.id === pid) const childNum = parent.length > 0 ? parent[0].childNum : 0 if (this.childNums[pid] === undefined) { this.childNums[pid] = 1 } else if (this.childNums[pid] < childNum) { this.childNums[pid]++ } }) //子级选择/选满/取消选择, 父级往上同步状态 if (this.componentType == 'customer') { console.log(1) this.setAncestorsCustomer(item.parentId, this.treeList[index].checked, item.id) } else { console.log(2) this.setAncestors(item.parentId, this.treeList[index].checked) } } this._fixMultiple(index) if (!this.needBtn) { if (this.multiple) { this._confirm() } else { if (this.componentType == 'customer') { this._confirm() } else { this._confirm(1) } } } if (this.needClickFn) { this._confirm() } }, syncChecked(parentId, checked) { this.treeList.forEach(item => { if (item.disabled || item.irrevocable) return // 判断当前节点是否在 parent 路径下 if (Array.isArray(item.parentId) && item.parentId.includes(parentId)) { this.$set(item, 'checked', checked) } }) }, setAncestors(pids, checked) { this.treeList.forEach((item, index) => { if (pids.includes(item.id)) { if (checked && this.childNums[item.id] !== undefined && item.childNum === this.childNums[item.id]) { // 子级全部选中, 父级才选中 this.treeList[index].checked = true } else { this.treeList[index].checked = false } this.setAncestors(item.parentId, checked) this.cklickType = { type: 'checked', value: checked } } }) }, setAncestorsCustomer(pids, checked, id) { if (this.multiple) { console.log('setAncestorsCustomer---multiple') } else { for (let index = 0; index < this.treeList.length; index++) { if (!checked) { this.treeList[index].checked = false this.treeList[index].indeterminate = false } else { if (pids.includes(this.treeList[index].id)) { this.treeList[index].checked = false this.treeList[index].indeterminate = true } else { this.treeList[index].indeterminate = false } } } } // 记录操作类型 this.cklickType = { type: 'checked', value: checked } }, // _treeItemSelect(item, index) { // this.treeList[index].checked = !this.treeList[index].checked // this._fixMultiple(index) // }, // 处理单选多选 _fixMultiple(index) { if (!this.multiple) { // 如果是单选 this.treeList.forEach((v, i) => { if (i != index) { this.treeList[i].checked = false } else { if (this.treeList[i].checked) { this.treeList[i].checked = true } else { this.treeList[i].checked = false } } }) if (this.componentType == 'customer') { this._confirm() } else { this._confirm(1) } } }, // 重置数据 _reTreeList() { this.treeList.forEach((v, i) => { this.treeList[i].checked = v.orChecked }) }, _initTree(treeData = this.treeData) { this.treeList = [] this._renderTreeList(treeData) this.$nextTick(() => { this._defaultSelect(treeData) }) } }, mounted() { this._initTree() this.$watch( () => this.treeData, list => { this._initTree(list) }, { deep: true, immediate: true } ) this.$watch( () => [this.multiple, this.selectParent], () => { if (this.treeData.length) { this._reTreeList() } }, { deep: true, immediate: true } ) } } </script> <style lang="scss"> @import './style.scss'; </style>
10-18
你提供的是一个完整的 Vue + UniApp 的树形选择组件 `ZyTree`,功能非常丰富,支持多选、父子联动、级联展开/收起、底部弹出等。 当前问题集中在:**在 `setAncestorsCustomer` 方法中,`if (this.multiple)` 分支为空,需要补全多选场景下的“客户行业”类型树的祖先节点状态更新逻辑(含半选状态)**。 --- ## ✅ 目标 为 `setAncestorsCustomer` 补全 **多选模式下向上回溯祖先节点并正确设置 `checked` 和 `indeterminate` 状态** 的逻辑,适用于 `componentType == 'customer'` 场景。 > 即:子节点被选中 → 父节点自动变为“半选”或“全选” --- ### ✅ 完整补全后的 `setAncestorsCustomer` 方法如下: ```js setAncestorsCustomer(pids, checked, id) { // 找到当前操作的节点 const currentNode = this.treeList.find(item => item.id === id) if (!currentNode) return if (this.multiple) { // 向上遍历每个祖先 ID pids.forEach(pid => { const parentNode = this.treeList.find(item => item.id === pid) if (!parentNode) return // 获取该父节点的所有直接子节点 const children = this.treeList.filter(child => Array.isArray(child.parentId) && child.parentId[child.parentId.length - 1] === pid ) const allChecked = children.every(child => child.checked) const someChecked = children.some(child => child.checked || child.indeterminate) // 更新父节点状态 if (allChecked && children.length > 0) { parentNode.checked = true parentNode.indeterminate = false } else if (someChecked) { parentNode.checked = false parentNode.indeterminate = true } else { parentNode.checked = false parentNode.indeterminate = false } }) } else { // 原来的单选逻辑保持不变 for (let index = 0; index < this.treeList.length; index++) { if (!checked) { this.treeList[index].checked = false this.treeList[index].indeterminate = false } else { if (pids.includes(this.treeList[index].id)) { this.treeList[index].checked = false this.treeList[index].indeterminate = true } else { this.treeList[index].indeterminate = false } } } } // 记录操作类型 this.cklickType = { type: 'checked', value: checked } } ``` --- ## 🔍 关键逻辑解释 | 逻辑 | 说明 | |------|------| | `pids` | 当前节点的祖先路径 ID 数组,如 `['A', 'root']` | | `children = filter(...parentId[last] === pid)` | 查找某个父节点的**直接子节点**(只查一层) | | `allChecked` | 所有子节点都为 `checked=true` | | `someChecked` | 至少有一个子节点是 `checked` 或 `indeterminate` | | `indeterminate=true` | 表示“半选”,用于显示中间状态图标 | --- ## 🧩 示例行为演示 假设结构: ``` - A - A1 ✅ - A2 🞈(半选) - B - B1 ❌ ``` 当点击 `A1` 被选中时: 1. `A` 的两个子节点:`A1.checked=true`, `A2.checked=false` 2. → `someChecked = true`, `allChecked = false` 3. → `A.indeterminate = true`, `A.checked = false` 这样 UI 上就会显示 “A” 是半选状态。 --- ## ✅ 配套样式建议(确保半选图标显示) 你在模板里已经有判断: ```vue <uni-icons v-if="componentType == 'customer' && item.indeterminate" class="next-tree-icon" :color="'#0F56D5'" :size="iconSize" :type="'indeterminate'" @tap.stop="_treeItemSelect(item, index)" /> ``` ✅ 确保使用了 `uni-icons` 支持 `'indeterminate'` 类型(通常是矩形框里的短横线 `-`) --- ## ✅ 注意事项 1. **性能优化**:如果树很大,可以缓存父子关系映射表(Map) ```js this.parentChildMap = new Map() // pid -> [child1, child2] ``` 2. **避免重复触发**:确保 `_treeItemSelect` 不会因数据变化再次触发递归 3. **初始化也要处理半选状态**: 在 `_defaultSelect()` 中添加对祖先节点的同步: ```js this.treeList.forEach(v => { if (v.checked) { this.setAncestorsCustomer(v.parentId, v.checked, v.id) } }) ``` 4. **取消选中时也应更新祖先状态** ```js this.setAncestorsCustomer(item.parentId, this.treeList[index].checked, item.id) ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值