【uni-app】树形结构数据选择框
该篇文章的写法采用的是v3写法,如果需要v2写法的 可以把代码复制进去让AI处理一下
效果预览:
组件图片分享:
主页面示例:
<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.textcloseModal();
};</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>
组件页面:
<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)"><imagev-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.showconst 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>