Uniapp+Vue3樹形選擇器示例代碼
想要開發(fā)一套代碼就能在多端運(yùn)行的應(yīng)用,用 UniApp + Vue 3 的組合,是目前開發(fā)效率和性能兼得的好選擇。
它不僅讓跨平臺(tái)開發(fā)更高效,也讓代碼本身更現(xiàn)代化、更健壯。下面這份全面的開發(fā)指南,希望能幫你快速上手。
為什么推薦 UniApp + Vue 3?
UniApp + Vue 3 的組合優(yōu)勢可以概括為以下幾點(diǎn):
| 維度 | 核心優(yōu)勢 |
|---|---|
| ?? 性能與效率 | Vue3版本默認(rèn)使用 Vite 構(gòu)建工具,項(xiàng)目冷啟動(dòng)速度相比Webpack版本可提升 3-5倍。配合 Vue 3 更快的響應(yīng)式系統(tǒng)和更小的運(yùn)行時(shí)體積-,應(yīng)用啟動(dòng)和交互響應(yīng)都更快。 |
| ?? 先進(jìn)開發(fā)體驗(yàn) | Vue 3 的 組合式 API (Composition API) 能實(shí)現(xiàn)更優(yōu)雅的邏輯復(fù)用與代碼組織,告別邏輯碎片化-。配合 TypeScript,大型項(xiàng)目開發(fā)和維護(hù)更加可靠。 |
| ?? 多端適配能力 | 得益于強(qiáng)大的 “條件編譯” 機(jī)制,可以為不同平臺(tái)(如微信小程序、App、H5)編寫專有代碼,在實(shí)現(xiàn)完全跨端的同時(shí),也能進(jìn)行精細(xì)化的平臺(tái)優(yōu)化。 |
| ?? 生態(tài)與未來 | 這是官方支持的默認(rèn)技術(shù)棧,當(dāng)前 UniApp 已全面擁抱 Vue 3-,未來也會(huì)持續(xù)更新。社區(qū)主流UI庫(如 uView Plus)和工具鏈都已提供成熟支持-。 |
下面通過示例代碼給大家詳細(xì)講解,如下所示:

<template>
<view class="tree-select">
<!-- 已選標(biāo)簽區(qū)域 + 全選按鈕 -->
<view v-if="showSelectedTags" class="selected-area">
<view class="selected-tags">
<view v-for="item in selectedNodes" :key="item[nodeKey]" class="tag">
<text class="tag-text">{{ item[labelField] }}</text>
<text class="tag-close" @click.stop="removeTag(item)">×</text>
</view>
</view>
<view class="action-buttons">
<view v-if="showSelectAll" class="select-all-btn" @click="toggleSelectAll">
{{ isAllSelected ? '取消全選' : '全選' }}
</view>
<view v-if="selectedNodes.length" class="clear-btn" @click="clearAll">清空</view>
</view>
</view>
<!-- 樹形區(qū)域 -->
<scroll-view class="tree-scroll" scroll-y>
<view v-if="flatList.length === 0" class="empty-tip">暫無數(shù)據(jù)</view>
<view
v-for="item in flatList"
:key="item[nodeKey]"
class="tree-item"
:style="{ paddingLeft: (item._level * 32 + 24) + 'rpx' }"
>
<!-- 展開/折疊圖標(biāo) -->
<view v-if="item._hasChildren" class="expand-icon" :class="{ expanded: item._expanded }" @click.stop="toggleExpand(item)">
{{ item._expanded ? '∨' : '>' }}
</view>
<view v-else class="expand-placeholder"></view>
<!-- 復(fù)選框 -->
<view class="checkbox" :class="{ checked: item._checked, indeterminate: item._indeterminate }" @click.stop="toggleCheck(item)">
<text v-if="item._checked">?</text>
<text v-else-if="item._indeterminate">—</text>
</view>
<!-- 標(biāo)簽 -->
<text class="node-label" :class="{ disabled: item[disabledField] }" @click.stop="toggleCheck(item)">
{{ item[labelField] }}
</text>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
const props = defineProps({
modelValue: { type: Array, default: () => [] },
treeData: { type: Array, default: () => [] },
nodeKey: { type: String, default: 'id' },
labelField: { type: String, default: 'label' },
childrenField: { type: String, default: 'children' },
disabledField: { type: String, default: 'disabled' },
expandAll: { type: Boolean, default: false },
showSelectedTags: { type: Boolean, default: true },
checkStrictly: { type: Boolean, default: false }, // false: 父子聯(lián)動(dòng); true: 獨(dú)立選擇
showSelectAll: { type: Boolean, default: true } // 是否顯示全選按鈕
})
const emit = defineEmits(['update:modelValue', 'change'])
// 內(nèi)部數(shù)據(jù)
const nodeMap = new Map()
let allNodes = []
const flatList = ref([])
const selectedSet = ref(new Set(props.modelValue))
// ---------- 構(gòu)建節(jié)點(diǎn)樹 ----------
function buildChildren(parent, childrenList, level) {
for (const raw of childrenList) {
const node = { ...raw }
node._level = level
node._parent = parent
node._checked = false
node._indeterminate = false
node._expanded = props.expandAll ? true : false
const grandChildren = node[props.childrenField]
const hasChildren = Array.isArray(grandChildren) && grandChildren.length > 0
node._hasChildren = hasChildren
node._children = hasChildren ? grandChildren : []
nodeMap.set(node[props.nodeKey], node)
if (hasChildren) {
buildChildren(node, grandChildren, level + 1)
}
}
}
function initData() {
nodeMap.clear()
const roots = []
for (const raw of props.treeData) {
const node = { ...raw }
node._level = 0
node._parent = null
node._checked = false
node._indeterminate = false
node._expanded = props.expandAll ? true : true
const childrenRaw = node[props.childrenField]
const hasChildren = Array.isArray(childrenRaw) && childrenRaw.length > 0
node._hasChildren = hasChildren
node._children = hasChildren ? childrenRaw : []
nodeMap.set(node[props.nodeKey], node)
roots.push(node)
if (hasChildren) {
buildChildren(node, childrenRaw, 1)
}
}
allNodes = Array.from(nodeMap.values())
// 根據(jù) modelValue 初始化選中狀態(tài)
const keys = props.modelValue || []
keys.forEach(key => {
const node = nodeMap.get(key)
if (node && !node[props.disabledField]) {
if (props.checkStrictly) {
setChecked(node, true)
} else {
setCheckedCascade(node, true)
}
}
})
// 更新半選狀態(tài)(僅在聯(lián)動(dòng)模式下)
if (!props.checkStrictly) {
const nodesByLevel = [...allNodes].sort((a,b) => b._level - a._level)
for (const node of nodesByLevel) {
if (node._parent) updateIndeterminate(node._parent)
}
}
updateFlatList()
emitChange()
}
function updateFlatList() {
const visible = []
function dfs(node) {
visible.push(node)
if (node._expanded && node._children.length) {
for (const childRaw of node._children) {
const child = nodeMap.get(childRaw[props.nodeKey])
if (child) dfs(child)
}
}
}
for (const node of allNodes) {
if (node._level === 0 && node._parent === null) {
dfs(node)
}
}
flatList.value = visible
}
function toggleExpand(node) {
node._expanded = !node._expanded
updateFlatList()
}
function setChecked(node, checked) {
node._checked = checked
if (checked) {
selectedSet.value.add(node[props.nodeKey])
} else {
selectedSet.value.delete(node[props.nodeKey])
}
}
function setCheckedCascade(node, checked) {
setChecked(node, checked)
node._indeterminate = false
if (node._children.length) {
for (const childRaw of node._children) {
const child = nodeMap.get(childRaw[props.nodeKey])
if (child) setCheckedCascade(child, checked)
}
}
}
function updateIndeterminate(node) {
if (props.checkStrictly) return
const children = node._children
if (!children.length) {
node._indeterminate = false
return
}
let checkedCount = 0, indeterminateCount = 0
for (const childRaw of children) {
const child = nodeMap.get(childRaw[props.nodeKey])
if (child) {
if (child._checked) checkedCount++
if (child._indeterminate) indeterminateCount++
}
}
if (checkedCount === children.length) {
if (!node._checked) setChecked(node, true)
node._indeterminate = false
} else if (checkedCount === 0 && indeterminateCount === 0) {
if (node._checked) setChecked(node, false)
node._indeterminate = false
} else {
if (node._checked) setChecked(node, false)
node._indeterminate = true
}
}
function updateAncestors(node) {
if (props.checkStrictly) return
let p = node._parent
while (p) {
updateIndeterminate(p)
p = p._parent
}
}
function toggleCheck(node) {
if (node[props.disabledField]) return
if (props.checkStrictly) {
const newVal = !node._checked
setChecked(node, newVal)
node._indeterminate = false
} else {
const newVal = !node._checked
setCheckedCascade(node, newVal)
updateAncestors(node)
}
updateFlatList()
emitChange()
}
// 全選/取消全選(統(tǒng)一處理兩種模式)
function toggleSelectAll() {
const shouldSelect = !isAllSelected.value
// 獲取所有可選節(jié)點(diǎn)(未禁用)
const selectableNodes = allNodes.filter(n => !n[props.disabledField])
if (props.checkStrictly) {
// 嚴(yán)格模式:直接設(shè)置每個(gè)節(jié)點(diǎn)的選中狀態(tài)
for (const node of selectableNodes) {
setChecked(node, shouldSelect)
node._indeterminate = false
}
} else {
// 聯(lián)動(dòng)模式:為避免重復(fù)級(jí)聯(lián),先清除所有選中,再設(shè)置根節(jié)點(diǎn)的選中狀態(tài)(級(jí)聯(lián)會(huì)帶動(dòng)子節(jié)點(diǎn))
// 但更高效的方式是直接設(shè)置所有節(jié)點(diǎn)的 _checked 為 shouldSelect,然后重新計(jì)算半選狀態(tài)。
// 因?yàn)槁?lián)動(dòng)模式下全選時(shí),所有節(jié)點(diǎn)都應(yīng)該選中,且沒有半選狀態(tài)。
// 直接設(shè)置所有節(jié)點(diǎn) _checked 和 _indeterminate
for (const node of allNodes) {
node._checked = shouldSelect
node._indeterminate = false
}
// 更新 selectedSet
selectedSet.value.clear()
if (shouldSelect) {
for (const node of allNodes) {
if (node._checked) selectedSet.value.add(node[props.nodeKey])
}
}
// 由于直接設(shè)置了所有節(jié)點(diǎn),無需再調(diào)用 setCheckedCascade,但需要更新選中集合
// 直接調(diào)用 emitChange 即可
emitChange()
updateFlatList()
return
}
emitChange()
updateFlatList()
}
// 判斷是否全選(所有可選節(jié)點(diǎn)都被選中)
const isAllSelected = computed(() => {
const selectableNodes = allNodes.filter(n => !n[props.disabledField])
if (selectableNodes.length === 0) return false
return selectableNodes.every(n => n._checked)
})
function emitChange() {
const keys = []
for (const node of allNodes) {
if (node._checked) keys.push(node[props.nodeKey])
}
selectedSet.value.clear()
keys.forEach(k => selectedSet.value.add(k))
emit('update:modelValue', keys)
const selectedObjs = keys.map(k => nodeMap.get(k)).filter(Boolean)
emit('change', keys, selectedObjs)
}
function removeTag(node) {
if (props.checkStrictly) {
setChecked(node, false)
node._indeterminate = false
} else {
setCheckedCascade(node, false)
updateAncestors(node)
}
updateFlatList()
emitChange()
}
function clearAll() {
for (const node of allNodes) {
node._checked = false
node._indeterminate = false
}
updateFlatList()
emitChange()
}
const selectedNodes = computed(() => {
return Array.from(selectedSet.value)
.map(key => nodeMap.get(key))
.filter(Boolean)
})
watch(() => props.treeData, () => {
initData()
}, { deep: true, immediate: true })
watch(() => props.modelValue, (newVal) => {
const newSet = new Set(newVal)
if (newSet.size !== selectedSet.value.size ||
!Array.from(newSet).every(k => selectedSet.value.has(k))) {
initData()
}
}, { deep: true })
onMounted(() => {
initData()
})
</script>
<style lang="scss" scoped>
.tree-select {
width: 100%;
background: #fff;
border-radius: 12rpx;
overflow: hidden;
.selected-area {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
border-bottom: 1rpx solid #eee;
background: #fafafa;
.selected-tags {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 16rpx;
.tag {
display: inline-flex;
align-items: center;
background: #e8f4ff;
border-radius: 8rpx;
padding: 8rpx 16rpx;
font-size: 24rpx;
color: #2979ff;
.tag-text {
max-width: 200rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tag-close {
margin-left: 8rpx;
font-size: 32rpx;
line-height: 1;
color: #999;
font-weight: bold;
&:active { color: #666; }
}
}
}
.action-buttons {
display: flex;
gap: 20rpx;
.select-all-btn, .clear-btn {
padding: 8rpx 16rpx;
font-size: 24rpx;
color: #2979ff;
background: #e8f4ff;
border-radius: 8rpx;
&:active { opacity: 0.7; }
}
.clear-btn {
color: #999;
background: #f0f0f0;
}
}
}
.tree-scroll {
max-height: 500rpx;
overflow-y: auto;
}
.empty-tip {
text-align: center;
padding: 60rpx 0;
color: #999;
font-size: 28rpx;
}
.tree-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
.expand-placeholder {
width: 48rpx;
height: 48rpx;
flex-shrink: 0;
}
.expand-icon {
width: 48rpx;
height: 48rpx;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #666;
&.expanded {
transform: rotate(0deg);
}
}
.checkbox {
width: 40rpx;
height: 40rpx;
flex-shrink: 0;
border-radius: 6rpx;
margin-right: 16rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: bold;
background: #fff;
border: 2rpx solid #ddd;
&.checked {
background: #2979ff;
border-color: #2979ff;
color: #fff;
}
&.indeterminate {
background: #2979ff;
border-color: #2979ff;
color: #fff;
font-size: 32rpx;
}
}
.node-label {
flex: 1;
font-size: 28rpx;
color: #333;
&.disabled {
color: #ccc;
}
}
}
}
</style>使用代碼
<template>
<TreeSelect
v-model="selectedIds"
:tree-data="menuTree"
node-key="id"
label-field="name"
children-field="children"
:check-strictly="true"
:show-select-all="true"
/>
</template>
<script setup>
import { ref } from 'vue'
import TreeSelect from "@/component/TreeSelect.vue";
const menuTree = ref([
{ id: 1, name: '總部', children: [
{ id: 11, name: '研發(fā)部' },
{ id: 12, name: '市場部', children: [
{ id: 121, name: '廣告組' }
]}
]},
{ id: 2, name: '分公司', children: [
{ id: 21, name: '銷售部' }
]}
])
const selectedIds = ref([1]);
</script>到此這篇關(guān)于Uniapp+Vue3樹形選擇器示例代碼的文章就介紹到這了,更多相關(guān)Uniapp Vue樹形選擇器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 基于Vue3+UniApp在單個(gè)頁面實(shí)現(xiàn)固定TabBar的多種方式
- uniapp+Vue3 組件之間的傳值方法示例詳解
- 基于uniapp?vue3?的滑動(dòng)搶單組件實(shí)例代碼
- UniApp在Vue3下使用setup語法糖創(chuàng)建和使用自定義組件的操作方法
- uniapp仿微信聊天界面效果實(shí)例(vue3組合式版本)
- uniapp+vue3實(shí)現(xiàn)上傳圖片組件封裝功能
- uniapp?vue3中使用webview在微信小程序?qū)崿F(xiàn)雙向通訊功能
- uniapp Vue3中如何解決web/H5網(wǎng)頁瀏覽器跨域的問題
- 基于uniapp+vue3自定義增強(qiáng)版table表格組件「兼容H5+小程序+App端」
相關(guān)文章
Vue3+Element Plus項(xiàng)目中使用html2canvas截圖方式
這篇文章主要介紹了Vue3+Element Plus項(xiàng)目中使用html2canvas截圖方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2026-03-03
使用vue腳手架(vue-cli)搭建一個(gè)項(xiàng)目詳解
這篇文章主要介紹了vue腳手架(vue-cli)搭建項(xiàng)目,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05
解決vue.js在編寫過程中出現(xiàn)空格不規(guī)范報(bào)錯(cuò)的問題
下面小編就為大家?guī)硪黄鉀Qvue.js在編寫過程中出現(xiàn)空格不規(guī)范報(bào)錯(cuò)的問題。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09
Vue 解決路由過渡動(dòng)畫抖動(dòng)問題(實(shí)例詳解)
這篇文章主要介紹了Vue 解決路由過渡動(dòng)畫抖動(dòng)問題,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-01-01
Vue動(dòng)畫事件詳解及過渡動(dòng)畫實(shí)例
通過 Vue.js 的過渡系統(tǒng),可以在元素從 DOM 中插入或移除時(shí)自動(dòng)應(yīng)用過渡效果。Vue.js 會(huì)在適當(dāng)?shù)臅r(shí)機(jī)為你觸發(fā) CSS 過渡或動(dòng)畫,你也可以提供相應(yīng)的 JavaScript 鉤子函數(shù)在過渡過程中執(zhí)行自定義的 DOM 操作2019-02-02
Create?vite理解Vite項(xiàng)目創(chuàng)建流程及代碼實(shí)現(xiàn)
這篇文章主要為大家介紹了Create?vite理解Vite項(xiàng)目創(chuàng)建流程及代碼實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
前端利用vue實(shí)現(xiàn)導(dǎo)入和導(dǎo)出功能詳細(xì)代碼
最近項(xiàng)目中讓實(shí)現(xiàn)一個(gè)導(dǎo)入導(dǎo)出Excel的功能,下面這篇文章主要給大家介紹了關(guān)于前端利用vue實(shí)現(xiàn)導(dǎo)入和導(dǎo)出功能的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-09-09

