【uni-app】树形结构数据选择框

该篇文章的写法采用的是v3写法,如果需要v2写法的 可以把代码复制进去让AI处理一下

效果预览:

组件图片分享:

主页面示例:

javascript 复制代码
<template>
  <view>
    <view>{{ selectedTypeName || '请选择' }}</view>

    <button @click="showDeviceTypeModal = true">选择类型</button>
    <!-- 自定义模态选择器 -->
    <view class="type-modal" @click="closeModal" v-if="showDeviceTypeModal">
       <view class="type-content" @click.stop>
          <view class="type-header">
             <text class="type-title">选择类型</text>
             <text class="type-close" @click="closeModal">×</text>
          </view>
          <scroll-view class="type-scroll" scroll-y="true" show-scrollbar="true">
             <view class="type-list">
                <view v-for="item in dataSource" :key="item.value">
                   <neo-tree-list-item :paramData="item" title="label" @tapText="handleItem" />
                </view>
             </view>
          </scroll-view>
       </view>
    </view>

  </view>
</template>

<script setup>
import { ref } from "vue";
import NeoTreeListItem from '@/components/neo-tree-list-item/neo-tree-list-item.vue';

// 假数据示例
const dataSource = [
   {
      text: '设备类型1',
      value: '1',
      children: [
         {
            text: '设备类型1-1',
            value: '1-1',
            children: [
                {
                   text: '设备类型1-1-1',
                   value: '1-1-1',
                   children: [
                          {
                            text: '设备类型1-1-1-1',
                            value: '1-1-1-1',
                          },
                          {
                            text: '设备类型1-1-1-2',
                            value: '1-1-1-2',
                          },
                          {
                            text: '设备类型1-1-1-3',
                          }
                   ]
                },
                {
                   text: '设备类型1-1-2',
                   value: '1-1-2',
                },
                {
                   text: '设备类型1-1-3',
                   value: '1-1-3',
                }
            ]
         },
         {
            text: '设备类型1-2',
            value: '1-2',
         },
         {
            text: '设备类型1-3',
            value: '1-3',
         },
         {
            text: '设备类型1-4',
            value: '1-4',
         }
      ]
   },
   {
      text: '设备类型2',
      value: '2',
   },
   {
      text: '设备类型3',
      value: '3',
   },
   {
      text: '设备类型4',
      value: '4',
   },
]

const showDeviceTypeModal = ref(false);
const closeModal = () => {
  showDeviceTypeModal.value = false;
};

const selectedTypeName = ref('');
const handleItem = (item) => {
  console.log(item);
  selectedTypeName.value =item.text
  closeModal();
};

</script>

<style lang="scss" scoped>

.type-modal {
   position: fixed;
   top: 0;
   left: 0;
   right: 0;
   bottom: 0;
   background-color: rgba(0, 0, 0, 0.6);
   z-index: 999;
   display: flex;
   justify-content: center;
   align-items: center;

   .type-content {
      width: 90%;
      max-height: 80%;
      background-color: #fff;
      border-radius: 16rpx;
      overflow: hidden;

      .type-header {
         display: flex;
         justify-content: space-between;
         align-items: center;
         padding: 20rpx 30rpx;
         border-bottom: 2rpx solid #f0f0f0;

         .type-title {
            font-size: 32rpx;
            font-weight: bold;
         }

         .type-close {
            font-size: 40rpx;
            color: #999;
         }
      }

      .type-scroll {
         max-height: 50vh;

         .type-list {
            padding-bottom: 20rpx;

            .type-item {
               display: flex;
               justify-content: space-between;
               align-items: center;
               padding: 20rpx 30rpx;
               border-bottom: 2rpx solid #f0f0f0;

               &.disabled {
                  color: #ccc;
               }

               &.selected {
                  color: #007fff;
               }

               .item-text {
                  font-size: 28rpx;
               }

               .check-icon {
                  margin-left: 20rpx;
               }
            }
         }
      }

      .type-footer {
         padding: 20rpx 30rpx;
         display: flex;
         justify-content: center;

         .confirm-btn {
            width: 100%;
            background-color: #007fff;
            color: #fff;
            border-radius: 10rpx;
         }
      }
   }
}

</style>

组件页面:

javascript 复制代码
<template>
  <view class="col-item" :class="{ 'col-item-bot': localShow }">
    <block v-if="paramData">
      <view class="col-item-title">
        <view 
          class="item-box" 
          :class="{ 'ch-item': currentLayer === 1 }"
          @click="handleItem(paramData)"
        >
          <image
            v-if="hasChildren"
            @click.stop="tapItemOne(paramData)"
            :class="localShow ? 'arrow-down-css' : 'arrow-right-css'"
            src="./image/arrow.png"
            class="arrow-icon"
          />
          <view class="item-box-left">
            <view class="left-images" v-show="currentLayer === 1">
            </view>
            <view>{{ paramData[title] || paramData.text }}</view>
          </view>
        </view>
      </view>

      <view 
        v-if="hasChildren && shouldRenderChildren" 
        v-show="localShow"
        class="children-container"
      >
        <view 
          v-for="item in paramData[children]" 
          :key="getItemKey(item)"
        >
          <neo-tree-list-item
            @parentEmit="parentEmit"
            :parentData="paramData"
            :title="title"
            :layer="currentLayer + 1"
            :paramData="item"
            @tapText="onTapText"
            @tapTitle="onTapTitle"
          />
        </view>
      </view>
    </block>
  </view>
</template>

<script setup>
import { ref, computed, watch } from 'vue';

const props = defineProps({
  paramData: {
    type: Object,
    default: undefined
  },
  parentData: {
    type: Object,
    default: undefined
  },
  title: {
    type: String,
    default: 'text'
  },
  children: {
    type: String,
    default: 'children'
  }
});

const emit = defineEmits(['tapText', 'tapTitle', 'parentEmit', 'update:show']);

const currentLayer = ref(0);
const hasRenderedChildren = ref(false);
const localShow = ref(props.paramData?.show || false);

const hasChildren = computed(() => {
  return props.paramData?.[props.children]?.length > 0;
});

const shouldRenderChildren = computed(() => {
  if (hasRenderedChildren.value) return true;
  if (localShow.value) {
    hasRenderedChildren.value = true;
    return true;
  }
  return false;
});

watch(
  () => props.paramData?.show,
  (newVal) => {
    localShow.value = newVal || false;
    if (newVal && !hasRenderedChildren.value) {
      hasRenderedChildren.value = true;
    }
  },
  { immediate: true }
);

const getItemKey = (item) => {
  return item.value || item.id || item.text || JSON.stringify(item);
};

const tapItemOne = (item) => {
  if (!hasChildren.value) {
    emit('tapTitle', item);
    return;
  }

  // 基于当前 localShow 状态来切换,而不是 item.show
  const newShowValue = !localShow.value;
  localShow.value = newShowValue;
  
  // 通知父组件更新状态
  emit('update:show', {
    item: item,
    show: newShowValue
  });
  
  if (newShowValue && item.created === undefined) {
    item.created = true;
    hasRenderedChildren.value = true;
  }
};

// 其他方法保持不变
const handleItem = (item) => {
  emit('tapText', item);
};

const onTapText = (item) => {
  emit('tapText', item);
};

const onTapTitle = (item) => {
  emit('tapTitle', item);
};

const parentEmit = () => {
  if (props.parentData) {
    emit('parentEmit');
  }
};

const recursionChecked = (item, checked) => {
  if (!item[props.children]) return;
  
  item[props.children].forEach(child => {
    child.checked = checked;
    recursionChecked(child, checked);
  });
};

watch(
  () => props.paramData,
  () => {
  },
  { deep: true }
);
</script>

<style scoped lang="scss">
.col-item {
  background: #ffffff;

  .col-item-title {
    display: flex;
    justify-content: flex-start;
  }
  
  .left-image {
    margin: 16rpx 0rpx 16rpx 32rpx;
    padding: 12rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 127, 255, 0.12);
    border-radius: 12rpx;
    
    .img {
      width: 60rpx;
    }
  }
  
  .item-box {
    height: 80rpx;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    width: 100%;
    padding: 0 32rpx;
    border-bottom: 2rpx solid rgba(126, 134, 142, 0.16);
    
    .item-box-left {
      display: flex;
      align-items: center;
      justify-content: flex-start;
      line-height: 40rpx;
    }
    
    .left-images {
      margin: 16rpx 0rpx;
      width: 40rpx;
      height: 40rpx;
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 12rpx;
      margin-right: 24rpx;
      
      .img {
        width: 60rpx;
      }
    }
  }
  
  .ch-item {
    border-bottom: 0;
    box-shadow: 124rpx 2rpx 0rpx rgba(126, 134, 142, 0.16);
  }
}

.col-item-bot {
  margin-bottom: 24rpx;
}

.arrow-down-css,
.arrow-right-css {
  width: 30rpx;
  height: 30rpx;
  margin-right: 1rpx;
  transition: transform 0.2s ease;
}

.arrow-down-css {
  transform: rotate(90deg);
}

.arrow-right-css {
  transform: rotate(0deg);
}

.children-container {
  padding-left: 60rpx; // 图标宽度+间距,确保所有子级对齐
}
</style>
相关推荐
云虎软件朱总2 小时前
配送跑腿系统:构建高并发、低延迟的同城配送系统架构解析
java·系统架构·uni-app
吃饺子不吃馅3 小时前
深感一事无成,还是踏踏实实做点东西吧
前端·svg·图形学
90后的晨仔3 小时前
Mac 上配置多个 Gitee 账号的完整教程
前端·后端
少年阿闯~~4 小时前
CSS——实现盒子在页面居中
前端·css·html
开发者小天4 小时前
uniapp中封装底部跳转方法
前端·javascript·uni-app
阿波罗尼亚4 小时前
复杂查询:直接查询/子查询/视图/CTE
java·前端·数据库
Q_Q19632884754 小时前
python+uniapp基于微信小程序的医院陪诊预约系统
开发语言·spring boot·python·微信小程序·django·flask·uni-app
正义的大古4 小时前
OpenLayers地图交互 -- 章节九:拖拽框交互详解
前端·vue.js·openlayers
三十_A5 小时前
【实录】使用 Verdaccio 从零搭建私有 npm 仓库(含完整步骤及避坑指南)
前端·npm·node.js