树形组件,支持搜索展示,自定义展示,支持vue2,vue3,小程序等等

发布于:2025-09-06 ⋅ 阅读:(22) ⋅ 点赞:(0)

效果图

平台兼容性

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
- - -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 快应用-华为 快应用-联盟
-
多语言 暗黑模式 宽屏模式
× ×

属性

属性名 类型 默认值 说明
sourceList Array [] 源数据,目前支持tree结构,
valueKey String id 指定 Object 中 key 的值作为节点数据id
textKey String name 指定 Object 中 key 的值作为节点显示内容
childrenKey String children 指定 Object 中 key 的值作为节点子集
multiple Boolean false 是否多选,默认单选
selectParent Boolean true 是否可以选父级,默认可以
placeholder String 标题
titleColor String 标题颜色
confirmColor String #0055ff 确定按钮颜色
cancelColor String #757575 取消按钮颜色
switchColor String #666 节点切换图标颜色
border Boolean false 是否有分割线,默认无
separator String / 选中项之间的分隔符

modelValue

String

 父组件传入的选中值(字符串,单选为单个ID,多选为分隔符连接的ID字符串)

returnParentWhenChildrenAllSelected Boolean false 子节点全选时是否返回父节点ID

方法

方法名 参数 默认值 说明
show() 显示选择器
hide() 隐藏选择器

组件引用:

<template>	
    <tree-search v-model="formData.other" :multiple='true' placeholder="请选择疾病"
									:sourceList="listData" valueKey="id" textKey="name" children="children" />
</template>

<script>
	import treeSearch from "@/components/tree-search/tree-search.vue"
	export default {
		components: {
			treeSearch
		},
		data() {
			return {
                listData: [{
						id: "1",
						name: "眼睑疾病",
						children: [
							{
								id: "1-1",
								name: "睑腺炎"
							},
							{
								id: "1-2",
								name: "睑板腺囊肿"
							},
							{
								id: "1-3",
								name: "睑缘炎",
								children: [
									{
										id: "1-3-1",
										name: "鳞屑性睑缘炎"
									},
									{
										id: "1-3-2",
										name: "溃疡性睑缘炎"
									},
									{
										id: "1-3-3",
										name: "眦部睑缘炎"
									}
								]
							},
							{
								id: "1-4",
								name: "眼睑湿疹"
							}
						]
					},
					{
						id: "2",
						name: "结膜疾病",
						children: [
							{
								id: "2-1",
								name: "结膜炎",
								children: [
									{
										id: "2-1-1",
										name: "细菌性结膜炎"
									},
									{
										id: "2-1-2",
										name: "细菌性结膜炎"
									},
									{
										id: "2-1-3",
										name: "衣原体性结膜炎"
									}
								]
							},
							{
								id: "2-2",
								name: "结膜结石"
							},
							{
								id: "2-3",
								name: "翼状胬肉"
							},
							{
								id: "2-4",
								name: "结膜下出血"
							},

						]
					},
					{
						id: "3",
						name: "角膜疾病",
						children: [{
								id: "3-1",
								name: "角膜炎",
								children: [{
										id: "3-1-1",
										name: "细菌性角膜炎"
									},
									{
										id: "3-1-2",
										name: "病毒性角膜炎"
									},
									{
										id: "3-1-3",
										name: "真菌性角膜炎"
									},
									{
										id: "3-1-4",
										name: "真菌性角膜炎"
									}
								]
							},
							{
								id: "3-2",
								name: "干眼综合征"
							},
							{
								id: "3-3",
								name: "暴露性角膜病变"
							}
						]
					}
				]
            }
        }
    }

</script>

定义组件:

<template>
  <view>
    <!-- 输入框,显示选中的名称,点击触发树形选择器显示 -->
    <uni-easyinput type="textarea" maxlength="-1" :autoHeight="true" v-model="selectedNames" :placeholder="placeholder" @focus="show()" :disabled="disabled" clearable/>
    <!-- 遮罩层,点击关闭对话框 -->
    <view class="tree-cover" :class="{'show':showDialog}" @tap="_cancel"></view>
    <!-- 树形选择器对话框 -->
    <view class="tree-dialog" :class="{'show':showDialog}">
      <view class="tree-bar">
        <!-- 取消按钮 -->
        <view class="tree-bar-cancel" :style="{'color':cancelColor}" hover-class="hover-c" @tap="_cancel">取消</view>
        <!-- 标题 -->
        <view class="tree-bar-title" :style="{'color':titleColor}">{{placeholder}}</view>
        <!-- 确认按钮,仅多选模式显示“确定”文字 -->
        <view class="tree-bar-confirm" :style="{'color':confirmColor}" hover-class="hover-c" @tap="_confirm">
          {{multiple?'确定':''}}
        </view>
      </view>
      <view class="tree-view">
        <!-- 树形列表容器,支持垂直滚动 -->
        <scroll-view class="tree-list" :scroll-y="true">
          <!-- 搜索输入框 -->
          <uni-easyinput confirmType="search" class="uni-mt-5" suffixIcon="search" v-model="searchValue" placeholder="输入关键字进行搜索"
            @iconClick="handleSearch" @change="handleSearch"></uni-easyinput>
          <!-- 循环渲染树节点 -->
          <block v-for="(item, index) in treeList" :key="index">
            <view class="tree-item" :style="[{paddingLeft: item.level*30 + 'rpx'}]" :class="{itemBorder: border === true, show: item.isShow}">
              <view class="item-label">
                <!-- 节点展开/折叠图标 -->
                <view class="item-icon uni-inline-item" @tap.stop="_onItemSwitch(item, index)">
                  <view v-if="!item.isLastLevel&&item.isShowChild" class="switch-on" :style="{'border-left-color':switchColor}"></view>
                  <view v-else-if="!item.isLastLevel&&!item.isShowChild" class="switch-off" :style="{'border-top-color':switchColor}"></view>
                  <view v-else-if="item.level === 0" class="item-last-dot" :style="{'border-top-color':switchColor}"></view>
                  <view v-else class="item-last-space"></view>
                </view>
                <!-- 节点选择区域 -->
                <view class="item-flex uni-flex-item uni-inline-item" @tap.stop="_onItemSelect(item, index)">
                  <view class="item-check" v-if="selectParent?true:item.isLastLevel">
                    <!-- 部分选中状态 -->
                    <view class="item-check-yes" v-if="item.checkStatus==1" :class="{'radio':!multiple}" :style="{'border-color':confirmColor}">
                      <view class="item-check-yes-part" :style="{'background-color':confirmColor}"></view>
                    </view>
                    <!-- 完全选中状态 -->
                    <view class="item-check-yes" v-else-if="item.checkStatus==2" :class="{'radio':!multiple}" :style="{'border-color':confirmColor}">
                      <view class="item-check-yes-all" :style="{'background-color':confirmColor}"></view>
                    </view>
                    <!-- 未选中状态 -->
                    <view class="item-check-no" v-else :class="{'radio':!multiple}" :style="{'border-color':confirmColor}"></view>
                  </view>
                  <!-- 节点名称 -->
                  <view class="item-name">{{item[this.textKey]}}</view>
                </view>
              </view>
            </view>
          </block>
        </scroll-view>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  name: "ba-tree-picker",
  props: {
    // 父组件传入的选中值(字符串,单选为单个ID,多选为分隔符连接的ID字符串)
    modelValue: {
      type: String,
      required: true,
      default: ''
    },
    // 节点唯一标识的字段名
    valueKey: {
      type: String,
      default: 'id'
    },
    // 节点显示名称的字段名
    textKey: {
      type: String,
      default: 'name'
    },
    // 子节点数组的字段名
    children: {
      type: String,
      default: 'children'
    },
    // 树形数据源
    sourceList: {
      type: Array,
      required: true,
      default: () => []
    },
    // 输入框占位符
    placeholder: {
      type: String,
      default: '请选择'
    },
    // 是否支持多选
    multiple: {
      type: Boolean,
      default: true
    },
    // 是否允许选择父节点
    selectParent: {
      type: Boolean,
      default: true
    },
    // 确认按钮颜色
    confirmColor: {
      type: String,
      default: '' // 默认 #0055ff
    },
    // 取消按钮颜色
    cancelColor: {
      type: String,
      default: '' // 默认 #757575
    },
    // 标题颜色
    titleColor: {
      type: String,
      default: ''
    },
    // 节点展开/折叠图标颜色
    switchColor: {
      type: String,
      default: '' // 默认 #666
    },
    // 选中项之间的分隔符
    separator: {
      type: String,
      default: '/'
    },
    // 是否显示节点分割线
    border: {
      type: Boolean,
      default: false
    },
    // 子节点全选时是否返回父节点ID
    returnParentWhenChildrenAllSelected: {
      type: Boolean,
      default: false
    },
	// 是否不可输入
	disabled: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      showDialog: false, // 控制对话框显示/隐藏
      treeList: [], // 扁平化的树形数据列表
      searchValue: '', // 搜索输入值
      selectedIds: [], // 选中的ID列表
      selectedNames: '', // 选中的名称字符串
      expandedNodes: new Set(), // 存储展开的节点ID
      selectedNodes: new Set(), // 存储选中的节点ID

    }
  },
  methods: {
    // 显示对话框并重置树显示状态
    show() {
      this.showDialog = true
      this._resetTreeDisplay()
      this._updateSelectedState() // 确保打开时选中状态正确
    },
    // 隐藏对话框并清空搜索
    _hide() {
      this.showDialog = false
      this.searchValue = ''
      this._resetTreeDisplay()
    },
    // 取消操作,触发取消事件并隐藏对话框
    _cancel() {
      this._hide()
      this.$emit("cancel")
    },
    // 确认选择,更新选中列表并触发父组件更新
    _confirm() {
		this._updateSelectedState(true)
		this._hide()
    },
    // 处理搜索逻辑,过滤显示匹配的节点
    handleSearch() {
      if (this.searchValue) {
        this.treeList.forEach(item => {
          const matchesSelf = item[this.textKey].toLowerCase().includes(this.searchValue.toLowerCase())
          const matchesChild = this._checkChildMatch(item, this.searchValue.toLowerCase())
          item.isShow = matchesSelf || matchesChild
          if (matchesSelf || matchesChild) {
            this._showParents(item) // 确保父节点显示
          }
        })
      } else {
        this._resetTreeDisplay() // 清空搜索时恢复默认显示
      }
      this._updateSelectedState() // 搜索后重新计算选中状态
    },
    // 检查子节点是否匹配搜索关键词(递归检查所有子孙节点)
    _checkChildMatch(item, keyword) {
      let hasMatch = false
      const itemIndex = this.treeList.findIndex(i => i[this.valueKey] === item[this.valueKey])
      if (itemIndex !== -1) {
        for (let i = itemIndex + 1; i < this.treeList.length; i++) {
          const childItem = this.treeList[i]
          if (childItem.level <= item.level) break
          
          // 检查当前子节点是否匹配
          if (childItem[this.textKey].toLowerCase().includes(keyword)) {
            hasMatch = true
            break
          }
          
          // 递归检查子节点的子节点
          if (!childItem.isLastLevel) {
            const childHasMatch = this._checkChildMatch(childItem, keyword)
            if (childHasMatch) {
              hasMatch = true
              break
            }
          }
        }
      }
      return hasMatch
    },
    // 显示节点的父节点链,确保搜索时父节点可见
    _showParents(item) {
      let current = item
      while (current.parentId !== -1) {
        const parent = this.treeList.find(p => p[this.valueKey] === current.parentId)
        if (parent) {
          parent.isShow = true
          parent.isShowChild = true
          this.expandedNodes.add(parent[this.valueKey])
          current = parent
        } else {
          break
        }
      }
    },
    // 重置树显示状态,恢复展开节点状态
    _resetTreeDisplay() {
      // 先重置所有节点状态
      this.treeList.forEach(item => {
        item.isShowChild = this.expandedNodes.has(item[this.valueKey]) && !item.isLastLevel
      })
      
      // 根节点默认显示
      this.treeList.forEach(item => {
        if (item.level === 0) {
          item.isShow = true
        } else {
          item.isShow = false
        }
      })
      
      // 确保展开节点的子节点正确显示
      this.treeList.forEach(item => {
        if (item.isShowChild) {
          const index = this.treeList.findIndex(i => i[this.valueKey] === item[this.valueKey])
          if (index !== -1) {
            this._showChildNodes(item, index)
          }
        }
      })
    },
    // 格式化树形数据,预加载所有子节点
    _formatTreeData(list = [], level = 0, parentItem = null, isShowChild = true) {
      if (!list || !Array.isArray(list)) return

      // 使用更简单的插入方式:在数组末尾直接添加
      let startIndex = this.treeList.length
      const parentId = parentItem ? parentItem[this.valueKey] : -1
      const modelValueArray = this._parseModelValue()

      list.forEach(item => {
        const isLastLevel = !item[this.children] || !Array.isArray(item[this.children]) || item[this.children].length === 0
        const isExpanded = this.expandedNodes.has(item[this.valueKey])

        let itemT = {
          [this.valueKey]: item[this.valueKey],
          [this.textKey]: item[this.textKey],
          level,
          isLastLevel,
          isShow: isShowChild,
          isShowChild: isExpanded && !isLastLevel,
          checkStatus: 0, // 默认未选中
          orCheckStatus: 0, // 初始选中状态
          parentId,
          children: item[this.children], // 保留原始children引用
          childCount: item[this.children] ? item[this.children].length : 0,
          childCheckCount: 0, // 子节点全选计数
          childCheckPCount: 0 // 子节点部分选中计数
        }

        // 根据modelValue设置选中状态
        if (modelValueArray.includes(String(itemT[this.valueKey]))) {
          itemT.checkStatus = 2
          itemT.orCheckStatus = 2
          itemT.childCheckCount = itemT.childCount
          this.selectedNodes.add(itemT[this.valueKey])
        }

        // 直接添加到数组末尾
        this.treeList.push(itemT)

        // 递归处理所有子节点(预加载)
        if (itemT.children && itemT.children.length > 0) {
          // 子节点是否显示应该基于当前节点是否显示,而不是展开状态
          this._formatTreeData(itemT.children, level + 1, itemT, itemT.isShow)
        }
      })

      // 递归完成后更新所有父节点的选中状态
      if (level === 0) {
        this.treeList.forEach(item => {
          if (!item.isLastLevel) {
            this._updateParentCheckStatus(item)
          }
        })
      }
    },
    // 解析modelValue为数组格式
    _parseModelValue() {
      let modelValueArray = []
      if (Array.isArray(this.modelValue)) {
        modelValueArray = this.modelValue.map(String)
      } else if (typeof this.modelValue === 'string' && this.modelValue.trim()) {
        modelValueArray = this.modelValue.split(this.separator).map(id => id.trim()).filter(id => id)
      } else if (typeof this.modelValue === 'number') {
        modelValueArray = [String(this.modelValue)]
      }
      return modelValueArray
    },
    // 更新父节点的选中状态
    _updateParentCheckStatus(item) {
      if (item.isLastLevel) return
      let childCheckCount = 0
      let childCheckPCount = 0
      for (let i = 0; i < this.treeList.length; i++) {
        const child = this.treeList[i]
        if (child.parentId === item[this.valueKey] && child.level === item.level + 1) {
          if (child.checkStatus === 2) childCheckCount++
          else if (child.checkStatus === 1) childCheckPCount++
        }
      }
      item.childCheckCount = childCheckCount
      item.childCheckPCount = childCheckPCount
      if (childCheckCount === item.childCount && childCheckPCount === 0) {
        item.checkStatus = 2
        item.orCheckStatus = 2
        this.selectedNodes.add(item[this.valueKey])
      } else if (childCheckCount > 0 || childCheckPCount > 0) {
        item.checkStatus = 1
        item.orCheckStatus = 1
      } else {
        item.checkStatus = 0
        item.orCheckStatus = 0
        this.selectedNodes.delete(item[this.valueKey])
      }
    },
    // 处理节点展开/折叠
    _onItemSwitch(item, index) {
      if (item.isLastLevel) return
      item.isShowChild = !item.isShowChild
      if (item.isShowChild) {
        this.expandedNodes.add(item[this.valueKey])
        // 展开时确保子节点可见
        this._showChildNodes(item, index)
      } else {
        this.expandedNodes.delete(item[this.valueKey])
        // 折叠时重置子节点显示状态,确保在搜索模式下也能正确折叠
        this._onItemChildSwitch(item, index)
      }
      this._updateSelectedState()
    },
    // 显示节点的所有子节点
    _showChildNodes(item, index) {
      const firstChildIndex = index + 1
      for (let i = firstChildIndex; i < this.treeList.length; i++) {
        let childItem = this.treeList[i]
        if (childItem.level <= item.level) break
        if (childItem.parentId === item[this.valueKey]) {
          // 在非搜索模式下,直接显示子节点
          if (!this.searchValue) {
            childItem.isShow = true
          } else {
            // 在搜索模式下,只有当子节点匹配搜索关键词或有匹配子节点时,才显示子节点
            // 检查子节点是否匹配搜索关键词
            const matchesSelf = childItem[this.textKey].toLowerCase().includes(this.searchValue.toLowerCase())
            // 或者检查子节点是否有匹配搜索关键词的子节点
            const matchesChild = this._checkChildMatch(childItem, this.searchValue.toLowerCase())
            
            // 关键修复:只有当子节点匹配搜索时才显示
            if (matchesSelf || matchesChild) {
              childItem.isShow = true
            }
          }
          // 如果子节点之前是展开状态,也显示其子节点
          if (childItem.isShowChild) {
            this._showChildNodes(childItem, i)
          }
        }
      }
    },
    // 更新子节点显示状态
    _onItemChildSwitch(item, index) {
      const firstChildIndex = index + 1
      for (let i = firstChildIndex; i < this.treeList.length; i++) {
        let childItem = this.treeList[i]
        if (childItem.level <= item.level) break
        if (childItem.parentId === item[this.valueKey]) {
          // 在非搜索模式下,根据父节点展开状态决定子节点显示状态
          if (!this.searchValue) {
            childItem.isShow = item.isShowChild
          } else {
            // 在搜索模式下,根据父节点展开状态和子节点匹配状态决定子节点显示状态
            // 检查子节点是否匹配搜索关键词
            const matchesSelf = childItem[this.textKey].toLowerCase().includes(this.searchValue.toLowerCase())
            // 或者检查子节点是否有匹配搜索关键词的子节点
            const matchesChild = this._checkChildMatch(childItem, this.searchValue.toLowerCase())
            
            // 关键修复:在搜索模式下,严格控制子节点显示
            if (item.isShowChild) {
              // 父节点展开时,只有子节点匹配才显示
              childItem.isShow = matchesSelf || matchesChild
            } else {
              // 父节点折叠时,无论子节点是否匹配,都隐藏
              // 这样可以确保折叠时真正收起所有子节点,保持搜索结果的一致性
              childItem.isShow = false
            }
          }
          if (!item.isShowChild) {
            childItem.isShowChild = false
            this.expandedNodes.delete(childItem[this.valueKey])
            // 递归折叠所有子节点
            this._onItemChildSwitch(childItem, i)
          }
        }
      }
    },
    // 处理节点选中/取消选中
    _onItemSelect(item, index) {
      if (!this.multiple) {
        // 单选模式:仅选中当前节点
        this.treeList.forEach((v, i) => {
          v.checkStatus = i === index ? 2 : 0
          v.orCheckStatus = v.checkStatus
          v.childCheckCount = v.checkStatus === 2 ? v.childCount : 0
          v.childCheckPCount = 0
          if (i === index) {
            this.selectedNodes.add(v[this.valueKey])
          } else {
            this.selectedNodes.delete(v[this.valueKey])
          }
        })
        this.selectedIds = [item[this.valueKey]]
        this.selectedNames = item[this.textKey]
        this.$emit('update:modelValue', item[this.valueKey])
        this._hide()
        return
      }

      // 多选模式:切换选中状态
      item.checkStatus = item.checkStatus === 0 ? 2 : 0
      item.orCheckStatus = item.checkStatus
      item.childCheckCount = item.checkStatus === 2 ? item.childCount : 0
      item.childCheckPCount = 0
      if (item.checkStatus === 2) {
        this.selectedNodes.add(item[this.valueKey])
      } else {
        this.selectedNodes.delete(item[this.valueKey])
      }

      // 更新子节点
      if (item.children && !item.isLastLevel) {
        this._onItemChildSelect(item, index)
      }
      
      // 更新父节点
      this._onItemParentSelect(item, index)
      
      // 更新选中状态和显示名称,不跳过发出事件,确保父节点选中状态能实时更新
      this._updateSelectedState()
    },
    // 更新子节点选中状态
    _onItemChildSelect(item, index) {
      for (let i = 0; i < this.treeList.length; i++) {
        let childItem = this.treeList[i]
        if (childItem.parentId === item[this.valueKey] && childItem.level === item.level + 1) {
          
          // 在搜索模式下,只更新和显示匹配搜索关键词的子节点
          if (this.searchValue) {
            // 检查子节点是否匹配搜索关键词或有匹配子节点
            const matchesSelf = childItem[this.textKey].toLowerCase().includes(this.searchValue.toLowerCase())
            const matchesChild = this._checkChildMatch(childItem, this.searchValue.toLowerCase())
            
            // 只有匹配的子节点才更新选中状态和显示
            if (matchesSelf || matchesChild) {
              childItem.checkStatus = item.checkStatus
              childItem.orCheckStatus = item.checkStatus
              childItem.childCheckCount = item.checkStatus === 2 ? childItem.childCount : 0
              childItem.childCheckPCount = 0
              if (item.checkStatus === 2) {
                this.selectedNodes.add(childItem[this.valueKey])
              } else {
                this.selectedNodes.delete(childItem[this.valueKey])
              }
              // 递归更新子节点的子节点
              this._onItemChildSelect(childItem, i)
            }
          } else {
            // 非搜索模式下,更新所有子节点
            childItem.checkStatus = item.checkStatus
            childItem.orCheckStatus = item.checkStatus
            childItem.childCheckCount = item.checkStatus === 2 ? childItem.childCount : 0
            childItem.childCheckPCount = 0
            if (item.checkStatus === 2) {
              this.selectedNodes.add(childItem[this.valueKey])
            } else {
              this.selectedNodes.delete(childItem[this.valueKey])
            }
            // 递归更新子节点的子节点
            this._onItemChildSelect(childItem, i)
          }
        }
      }
    },
    // 更新父节点选中状态
    _onItemParentSelect(item, index) {
      const parentIndex = this.treeList.findIndex(itemP => itemP[this.valueKey] === item.parentId)
      if (parentIndex >= 0) {
        let parent = this.treeList[parentIndex]
        this._updateParentCheckStatus(parent)
        this._onItemParentSelect(parent, parentIndex)
      }
    },
    // 更新选中状态和显示名称
    _updateSelectedState(skipEmit = false) {
      // 发出更新事件
      if (skipEmit) {
		this.selectedIds = []
		this.selectedNames = ''

		if (this.returnParentWhenChildrenAllSelected) {
			// 当子节点全选时返回父节点
			let currentLevel = -1
			this.treeList.forEach(item => {
			if (currentLevel >= 0 && item.level > currentLevel) return
			if (item.checkStatus === 2) {
				currentLevel = item.level
				this.selectedIds.push(item[this.valueKey])
				this.selectedNames = this.selectedNames
				? `${this.selectedNames} ${this.separator} ${item[this.textKey]}`
				: item[this.textKey]
			} else {
				currentLevel = -1
			}
			})
		} else {
			// 返回所有选中的子节点
			this.treeList.forEach(item => {
			if (item.checkStatus === 2 && (!item.children || item.isLastLevel)) {
				this.selectedIds.push(item[this.valueKey])
				this.selectedNames = this.selectedNames
				? `${this.selectedNames} ${this.separator} ${item[this.textKey]}`
				: item[this.textKey]
			}
			})
		}
        this.$emit('update:modelValue', this.multiple ? this.selectedIds.join(this.separator) : (this.selectedIds[0] || ''))
      }
    },
    // 初始化树形结构
    _initTree() {
      // 重置状态
      this.treeList = []
      this.expandedNodes.clear()
      this.selectedNodes.clear()
      
      // 格式化树数据
      this._formatTreeData(this.sourceList)
      
      // 根据modelValue设置选中状态并展开相关节点
      const modelValueArray = this._parseModelValue()
      if (modelValueArray.length > 0) {
        const validIds = new Set(this.treeList.map(item => String(item[this.valueKey])))
        const foundNames = []
        
        modelValueArray.forEach(id => {
          if (validIds.has(id)) {
            const foundItem = this.treeList.find(item => String(item[this.valueKey]) === id)
            if (foundItem) {
              // 设置选中状态
              foundItem.checkStatus = 2
              foundItem.orCheckStatus = 2
              this.selectedNodes.add(id)
              foundNames.push(foundItem[this.textKey])
              
              // 确保选中节点的所有父节点都展开
              let current = foundItem
              while (current.parentId !== -1) {
                const parent = this.treeList.find(p => p[this.valueKey] === current.parentId)
                if (parent) {
                  this.expandedNodes.add(parent[this.valueKey])
                  current = parent
                } else {
                  break
                }
              }
            }
          }
        })
        
        if (foundNames.length > 0) {
          this.selectedNames = foundNames.join(` ${this.separator} `)
        }
      }
      
      // 更新所有父节点的选中状态
      this.treeList.forEach(item => {
        if (!item.isLastLevel) {
          this._updateParentCheckStatus(item)
        }
      })
      
      this._resetTreeDisplay()
    }
  },
  watch: {
    // 监听sourceList变化,重新初始化树
    sourceList: {
      handler() {
        this._initTree()
      },
      deep: true
    },
    // 监听modelValue变化,更新树状态
    modelValue: {
      immediate: true,
      handler() {
        this._initTree()
      }
    }
  },
  // 组件挂载时初始化树
  mounted() {
    this._initTree()
  }
}
</script>

<style scoped>
/* 样式保持完全不变 */
:deep(.uni-easyinput) {
  width: 100%;
  position: relative;
  text-align: left;
  color: #333;
  font-size: 14px;
}

.tree-cover {
  position: fixed;
  top: 0rpx;
  right: 0rpx;
  bottom: 0rpx;
  left: 0rpx;
  z-index: 100;
  background-color: rgba(0, 0, 0, .4);
  opacity: 0;
  transition: all 0.3s ease;
  visibility: hidden;
}

.tree-cover.show {
  visibility: visible;
  opacity: 1;
}

.tree-dialog {
  position: fixed;
  top: 0rpx;
  right: 0rpx;
  bottom: 0rpx;
  left: 0rpx;
  background-color: #fff;
  border-top-left-radius: 10px;
  border-top-right-radius: 10px;
  display: flex;
  flex-direction: column;
  z-index: 102;
  top: 20%;
  transition: all 0.3s ease;
  transform: translateY(100%);
}

.tree-dialog.show {
  transform: translateY(0);
}

.tree-bar {
  height: 90rpx;
  padding-left: 25rpx;
  padding-right: 25rpx;
  display: flex;
  justify-content: space-between;
  align-items: center;
  box-sizing: border-box;
  border-bottom-width: 1rpx !important;
  border-bottom-style: solid;
  border-bottom-color: #f5f5f5;
  font-size: 32rpx;
  color: #757575;
  line-height: 1;
}

.tree-bar-confirm {
  color: #0055ff;
  padding: 15rpx;
}

.tree-bar-title {
	font-weight: bold;
  color: #333;
}

.tree-bar-cancel {
  color: #757575;
  padding: 15rpx;
}

.tree-view {
  flex: 1;
  padding: 20rpx;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  height: 100%;
}

.item-flex {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.tree-list {
  flex: 1;
  height: 100%;
  overflow: hidden;
}

.tree-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  line-height: 1;
  height: 0;
  opacity: 0;
  transition: 0.2s;
  overflow: hidden;
}

.tree-item.show {
  height: 90rpx;
  opacity: 1;
}

.tree-item.showchild:before {
  transform: rotate(90deg);
}

.tree-item.last:before {
  opacity: 0;
}

.switch-on {
      width: 0;
      height: 0;
      border-left: 10rpx solid transparent;
      border-right: 10rpx solid transparent;
      border-top: 15rpx solid #666;
    }

    .switch-off {
      width: 0;
      height: 0;
      border-bottom: 10rpx solid transparent;
      border-top: 10rpx solid transparent;
      border-left: 15rpx solid #666;
    }

.item-last-dot {
  position: absolute;
  width: 10rpx;
  height: 10rpx;
  border-radius: 100%;
  background: #666;
}

.item-last-space {
  width: 10rpx;
  height: 10rpx;
}

.item-icon {
  width: 26rpx;
  height: 26rpx;
  margin-right: 8rpx;
  padding-right: 20rpx;
  padding-left: 20rpx;
}

.item-label {
  flex: 1;
  display: flex;
  align-items: center;
  height: 100%;
  line-height: 1.2;
}

.item-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  width: 450rpx;
}

.item-check {
  width: 40px;
  height: 40px;
  display: flex;
  justify-content: center;
  align-items: center;
}

.item-check-yes,
.item-check-no {
  width: 20px;
  height: 20px;
  border-top-left-radius: 20%;
  border-top-right-radius: 20%;
  border-bottom-right-radius: 20%;
  border-bottom-left-radius: 20%;
  border-top-width: 1rpx;
  border-left-width: 1rpx;
  border-bottom-width: 1rpx;
  border-right-width: 1rpx;
  border-style: solid;
  border-color: #0055ff;
  display: flex;
  justify-content: center;
  align-items: center;
  box-sizing: border-box;
}

.item-check-yes-part {
  width: 12px;
  height: 12px;
  border-top-left-radius: 20%;
  border-top-right-radius: 20%;
  border-bottom-right-radius: 20%;
  border-bottom-left-radius: 20%;
  background-color: #0055ff;
}

.item-check-yes-all {
  margin-bottom: 5px;
  border: 2px solid #007aff;
  border-left: 0;
  border-top: 0;
  height: 12px;
  width: 6px;
  transform-origin: center;
  transition: all 0.3s;
  transform: rotate(45deg);
}

.item-check .radio {
  border-top-left-radius: 50%;
  border-top-right-radius: 50%;
  border-bottom-right-radius: 50%;
  border-bottom-left-radius: 50%;
}

.item-check .radio .item-check-yes-b {
  border-top-left-radius: 50%;
  border-top-right-radius: 50%;
  border-bottom-right-radius: 50%;
  border-bottom-left-radius: 50%;
}

.hover-c {
  opacity: 0.6;
}

.itemBorder {
  border-bottom: 1px solid #e5e5e5;
}
</style>


网站公告

今日签到

点亮在社区的每一天
去签到