Vue3中Tree樹組件開發(fā)文檔(附使用示例)

一、組件概述
這個樹組件基于 Vue3 和 Composition API 構建,提供了強大的節(jié)點選擇功能,支持單選、多選模式,以及父子節(jié)點關聯(lián)或獨立的選擇邏輯。組件設計靈活,可定制性強,適用于多種場景。
二、核心功能
1. 選擇模式
- 單選模式:每次只能選中一個節(jié)點,點擊已選中節(jié)點會取消選擇
- 多選模式:可以同時選中多個節(jié)點,通過復選框或點擊節(jié)點進行操作
2. 父子節(jié)點關聯(lián)
- 關聯(lián)模式:父節(jié)點選中時,所有子節(jié)點自動選中;子節(jié)點全部選中時,父節(jié)點自動選中
- 獨立模式:父子節(jié)點選擇狀態(tài)互不影響,可單獨控制
3. 節(jié)點操作
- 點擊節(jié)點:根據(jù)配置可觸發(fā)選擇操作
- 展開 / 折疊:可通過圖標控制節(jié)點的展開與折疊狀態(tài)
- 禁用節(jié)點:支持設置某些節(jié)點為禁用狀態(tài),不可選擇
三、組件結構
1. 主要組件
Tree.vue:樹組件主體,負責整體結構和狀態(tài)管理TreeNode.vue:樹節(jié)點組件,負責單個節(jié)點的渲染和交互
2. 核心屬性
treeData:樹結構數(shù)據(jù),包含節(jié)點的 id、標簽、子節(jié)點等信息modelValue:雙向綁定的選中節(jié)點值keyMap:自定義節(jié)點數(shù)據(jù)的鍵名映射config:配置項,控制多選、父子關聯(lián)等功能
四、使用示例
1. 基礎用法
<Tree :treeData="treeData" v-model="checkedKeys" :keyMap="keyMap" :config="config" />
2. 完整示例代碼
index.vue
<template>
<div class="container">
<h3>樹組件示例</h3>
<div class="options">
<label>
<input type="checkbox" v-model="config.multiple" /> 多選模式
</label>
<label>
<input type="checkbox" v-model="config.checkStrictly" /> 父子節(jié)點不關聯(lián)
</label>
<label>
<input type="checkbox" v-model="config.expandAll" /> 全部展開
</label>
<label>
<input type="checkbox" v-model="config.showCheckbox" /> 顯示復選框
</label>
<button @click="getAllCheckedNodes">獲取所有選中節(jié)點</button>
</div>
<div class="tree-wrapper">
<Tree
ref="treeRef"
:treeData="treeData"
v-model="checkedKeys"
:keyMap="keyMap"
:config="config"
@node-check="handleNodeCheck"
/>
</div>
<div class="selected-result">
<h4>選中結果:</h4>
<pre>{{ checkedKeys }}</pre>
</div>
<div class="last-selected">
<h4>最后操作節(jié)點:</h4>
<pre v-if="lastSelectedNode">{{ lastSelectedNode[keyMap.id] }} - {{ lastSelectedNode[keyMap.label] }} ({{ lastSelectedStatus ? '選中' : '取消選中' }})</pre>
<p v-else>無操作</p>
</div>
<div class="all-checked-nodes" v-if="allCheckedNodes.length > 0">
<h4>所有選中節(jié)點:</h4>
<pre>{{ allCheckedNodes }}</pre>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import Tree from './Tree.vue';
const treeRef = ref(null);
const allCheckedNodes = ref([]);
const treeData = ref([
{
id: 1,
name: '一級節(jié)點1',
children: [
{
id: 4,
name: '二級節(jié)點1-1',
children: [
{ id: 9, name: '三級節(jié)點1-1-1', disabled: true },
{ id: 10, name: '三級節(jié)點1-1-2' }
]
},
{ id: 5, name: '二級節(jié)點1-2' }
]
},
{
id: 2,
name: '一級節(jié)點2',
children: [
{ id: 6, name: '二級節(jié)點2-1' },
{ id: 7, name: '二級節(jié)點2-2' }
]
},
{
id: 3,
name: '一級節(jié)點3',
disabled: true
}
]);
const checkedKeys = ref([4, 7]);
const keyMap = ref({
id: 'id',
label: 'name',
children: 'children',
disabled: 'disabled'
});
const config = ref({
multiple: true,
checkStrictly: false,
expandAll: false,
showCheckbox: true
});
const lastSelectedNode = ref(null);
const lastSelectedStatus = ref(false);
const handleNodeCheck = (node, isChecked) => {
lastSelectedNode.value = node;
lastSelectedStatus.value = isChecked;
console.log('節(jié)點勾選變化:', node, '選中狀態(tài):', isChecked);
};
const getAllCheckedNodes = () => {
if (treeRef.value) {
try {
allCheckedNodes.value = treeRef.value.getCheckedNodes();
console.log('所有選中節(jié)點:', allCheckedNodes.value);
} catch (error) {
console.error('獲取選中節(jié)點時出錯:', error);
alert('獲取選中節(jié)點時出錯: ' + error.message);
}
}
};
</script>
<style>
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.options {
margin-bottom: 20px;
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
}
button {
padding: 6px 12px;
background-color: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: #66b1ff;
}
.tree-wrapper {
border: 1px solid #e5e7eb;
border-radius: 4px;
margin-bottom: 20px;
max-height: 400px;
overflow-y: auto;
}
.selected-result, .last-selected, .all-checked-nodes {
background-color: #f9fafb;
padding: 15px;
border-radius: 4px;
margin-bottom: 15px;
}
</style>
Tree.vue
<script setup>
import { computed, defineComponent, ref, watch } from 'vue'
import TreeNode from './TreeNode.vue'
const props = defineProps({
treeData: {
type: Array,
required: true,
},
modelValue: {
type: [Array, String, Number, null],
default: () => [],
},
keyMap: {
type: Object,
default: () => ({
id: 'id',
label: 'label',
children: 'children',
disabled: 'disabled',
}),
},
config: {
type: Object,
default: () => ({
multiple: false,
checkStrictly: false,
expandAll: false,
showCheckbox: true,
}),
},
})
const emit = defineEmits(['update:modelValue', 'nodeCheck'])
function handleCheckChange(value) {
emit('update:modelValue', value)
}
function handleNodeCheck(node, isChecked) {
emit('nodeCheck', node, isChecked)
}
// 獲取所有選中節(jié)點對象的方法
function getCheckedNodes() {
const checkedIds = Array.isArray(props.modelValue) ? props.modelValue : [props.modelValue].filter(id => id !== null)
const result = []
const traverse = (nodes) => {
nodes.forEach((node) => {
if (checkedIds.includes(node[props.keyMap.id])) {
// 創(chuàng)建節(jié)點的副本,避免循環(huán)引用
const cleanNode = { ...node }
if (cleanNode.parent) {
// 移除parent引用,避免循環(huán)
delete cleanNode.parent
}
if (cleanNode[props.keyMap.children]) {
// 遞歸處理子節(jié)點
cleanNode[props.keyMap.children] = cleanNode[props.keyMap.children].map((child) => {
const cleanChild = { ...child }
delete cleanChild.parent
return cleanChild
})
}
result.push(cleanNode)
}
if (node[props.keyMap.children] && node[props.keyMap.children].length > 0) {
traverse(node[props.keyMap.children])
}
})
}
traverse(props.treeData)
return result
}
// 監(jiān)聽配置變化,當從多選切換到單選時清空選中狀態(tài)
watch(() => props.config.multiple, (newVal, oldVal) => {
if (oldVal && !newVal) {
// 從多選切換到單選,清空選中狀態(tài)
emit('update:modelValue', null)
}
})
// 提供對外方法
defineExpose({
getCheckedNodes,
})
</script>
<template>
<div class="tree-container">
<ul class="tree-list">
<TreeNode
v-for="node in treeData"
:key="node[keyMap.id]"
:node="node"
:key-map="keyMap"
:config="config"
:checked-keys="modelValue"
@update:model-value="handleCheckChange"
@node-check="handleNodeCheck"
/>
</ul>
</div>
</template>
<style scoped>
.tree-container {
padding: 8px;
border-radius: 4px;
}
.tree-list {
list-style: none;
padding: 0;
margin: 0;
}
</style>
TreeNode.vue
<script setup>
import { computed, defineComponent, ref, watch } from 'vue'
const props = defineProps({
node: {
type: Object,
required: true,
},
keyMap: {
type: Object,
required: true,
},
config: {
type: Object,
required: true,
},
checkedKeys: {
type: [Array, String, Number, null],
default: () => [],
},
})
const emit = defineEmits(['update:modelValue', 'nodeCheck'])
const isExpanded = ref(props.config.expandAll)
const hasChildren = computed(() => Array.isArray(props.node[props.keyMap.children]) && props.node[props.keyMap.children].length > 0)
const isDisabled = computed(() => props.node[props.keyMap.disabled] === true)
// 節(jié)點選中狀態(tài)
const isChecked = computed({
get() {
if (props.config.multiple) {
return Array.isArray(props.checkedKeys) && props.checkedKeys.includes(props.node[props.keyMap.id])
}
else {
return props.checkedKeys === props.node[props.keyMap.id]
}
},
set(value) {
handleCheck(value)
},
})
// 計算子節(jié)點選中狀態(tài)
const childrenCheckedState = computed(() => {
if (!hasChildren.value || !props.config.multiple)
return 'none'
const children = props.node[props.keyMap.children]
const checkedCount = children.filter(child =>
Array.isArray(props.checkedKeys) && props.checkedKeys.includes(child[props.keyMap.id]),
).length
if (checkedCount === 0)
return 'none'
if (checkedCount === children.length)
return 'all'
return 'partial'
})
// 父子關聯(lián)邏輯
function handleCheck(newState) {
if (isDisabled.value)
return
const nodeId = props.node[props.keyMap.id]
let newCheckedKeys
// 選中狀態(tài)切換
const isNodeChecked = isChecked.value
const shouldCheck = newState !== undefined ? newState : !isNodeChecked
if (props.config.multiple) {
// 多選模式
newCheckedKeys = Array.isArray(props.checkedKeys) ? [...props.checkedKeys] : []
// 多選模式且父子關聯(lián)
if (!props.config.checkStrictly) {
// 更新子節(jié)點
const updateChildren = (children, checkState) => {
children.forEach((child) => {
if (!child[props.keyMap.disabled]) {
const childId = child[props.keyMap.id]
if (checkState) {
if (!newCheckedKeys.includes(childId)) {
newCheckedKeys.push(childId)
}
}
else {
newCheckedKeys = newCheckedKeys.filter(id => id !== childId)
}
if (child[props.keyMap.children] && child[props.keyMap.children].length > 0) {
updateChildren(child[props.keyMap.children], checkState)
}
}
})
}
// 更新父節(jié)點
const updateParent = (node, checkState) => {
if (!node.parent)
return
const parentId = node.parent[props.keyMap.id]
const siblings = node.parent[props.keyMap.children]
// 檢查所有兄弟節(jié)點的選中狀態(tài)
const allSiblingsChecked = siblings.every(sib =>
sib[props.keyMap.disabled] || newCheckedKeys.includes(sib[props.keyMap.id]),
)
const noSiblingsChecked = siblings.every(sib =>
sib[props.keyMap.disabled] || !newCheckedKeys.includes(sib[props.keyMap.id]),
)
// 如果所有兄弟都被選中,則父節(jié)點也應被選中
// 如果沒有兄弟被選中,則父節(jié)點也應被取消選中
if (allSiblingsChecked) {
if (!newCheckedKeys.includes(parentId)) {
newCheckedKeys.push(parentId)
emit('nodeCheck', node.parent, true)
}
}
else if (noSiblingsChecked) {
newCheckedKeys = newCheckedKeys.filter(id => id !== parentId)
emit('nodeCheck', node.parent, false)
}
// 遞歸更新上層父節(jié)點
updateParent(node.parent, checkState)
}
// 處理子節(jié)點
if (hasChildren.value) {
updateChildren(props.node[props.keyMap.children], shouldCheck)
}
// 處理父節(jié)點
if (props.node.parent) {
updateParent(props.node, shouldCheck)
}
}
// 更新當前節(jié)點
if (shouldCheck) {
if (!newCheckedKeys.includes(nodeId)) {
newCheckedKeys.push(nodeId)
}
}
else {
newCheckedKeys = newCheckedKeys.filter(id => id !== nodeId)
}
}
else {
// 單選模式
newCheckedKeys = shouldCheck ? nodeId : null
}
emit('update:modelValue', newCheckedKeys)
emit('nodeCheck', props.node, shouldCheck)
}
function toggleExpand() {
if (!isDisabled.value) {
isExpanded.value = !isExpanded.value
}
}
function handleNodeClick() {
if (!props.config.showCheckbox && !isDisabled.value) {
handleCheck()
}
}
function handleChildCheckChange(newCheckedKeys) {
emit('update:modelValue', newCheckedKeys)
}
function handleChildNodeCheck(node, isChecked) {
emit('nodeCheck', node, isChecked)
}
// 添加父節(jié)點引用
watch(() => props.node[props.keyMap.children], (children) => {
if (children && children.length > 0) {
children.forEach((child) => {
child.parent = props.node
})
}
}, { immediate: true, deep: true })
watch(() => props.config.expandAll, (newVal) => {
isExpanded.value = newVal
})
</script>
<template>
<li class="tree-node">
<div
class="node-content"
:class="{
'node-expanded': isExpanded,
'node-selected': isChecked,
'node-disabled': isDisabled,
}"
>
<span
v-if="hasChildren"
class="expand-icon"
@click.stop="toggleExpand"
>
{{ isExpanded ? '?' : '?' }}
</span>
<input
v-if="config.showCheckbox"
type="checkbox"
:checked="isChecked"
:disabled="isDisabled"
@change.stop="handleCheck"
>
<span class="node-label" @click.stop="handleNodeClick">{{ node[keyMap.label] }}</span>
</div>
<ul
v-if="hasChildren && isExpanded"
class="children-list"
>
<TreeNode
v-for="child in node[keyMap.children]"
:key="child[keyMap.id]"
:node="child"
:key-map="keyMap"
:config="config"
:checked-keys="checkedKeys"
@update:model-value="handleChildCheckChange"
@node-check="handleChildNodeCheck"
/>
</ul>
</li>
</template>
<style scoped>
.tree-node {
margin: 4px 0;
}
.node-content {
display: flex;
align-items: center;
padding: 4px 8px;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.2s;
}
.node-content:hover:not(.node-disabled) {
background-color: #f5f7fa;
}
.node-expanded .expand-icon {
transform: rotate(90deg);
}
.expand-icon {
display: inline-block;
width: 16px;
text-align: center;
margin-right: 4px;
transition: transform 0.2s;
}
.node-label {
margin-left: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.node-selected {
background-color: #e6f7ff;
}
.node-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.children-list {
list-style: none;
padding-left: 20px;
margin: 0;
overflow: hidden;
transition: max-height 0.3s ease-in-out;
}
</style>
五、配置詳解
const config = {
multiple: true, // 是否啟用多選模式
checkStrictly: false, // 是否父子節(jié)點不關聯(lián)
expandAll: false, // 是否全部展開
showCheckbox: true // 是否顯示復選框
};
3. 鍵名映射
const keyMap = {
id: 'id', // 節(jié)點ID的鍵名
label: 'name', // 節(jié)點顯示文本的鍵名
children: 'children', // 子節(jié)點數(shù)組的鍵名
disabled: 'disabled' // 禁用狀態(tài)的鍵名
};
六、事件處理
<Tree @node-check="handleNodeCheck" />
const handleNodeCheck = (node, isChecked) => {
// node: 被操作的節(jié)點對象
// isChecked: 當前的選中狀態(tài) (true/false)
console.log('節(jié)點勾選變化:', node, '選中狀態(tài):', isChecked);
};
2. 獲取所有選中節(jié)點
const getAllCheckedNodes = () => {
if (treeRef.value) {
const nodes = treeRef.value.getCheckedNodes();
console.log('所有選中節(jié)點:', nodes);
}
};
七、常見問題與解決方案
1. 多選模式切換問題
- 問題描述:從多選切換到單選時,之前的選中狀態(tài)未清除
- 解決方案:組件已自動處理,切換模式時會清空選中狀態(tài)
2. 循環(huán)引用錯誤
- 問題描述:調(diào)用
getCheckedNodes方法時出現(xiàn)循環(huán)引用錯誤 - 解決方案:組件內(nèi)部已處理,返回的節(jié)點數(shù)據(jù)不包含循環(huán)引用
3. 父子節(jié)點關聯(lián)邏輯
- 問題描述:父節(jié)點與子節(jié)點的選擇狀態(tài)不一致
- 解決方案:確保
checkStrictly配置正確,關聯(lián)模式下會自動同步父子節(jié)點狀態(tài)
八、擴展與定制
1. 自定義節(jié)點樣式
可以通過修改TreeNode.vue中的樣式類來定制節(jié)點外觀,也可以添加自定義類。
2. 添加新功能
可以在組件基礎上擴展新功能,如搜索過濾、拖拽排序等。
3. 事件擴展
可以添加新的事件,如節(jié)點展開 / 折疊事件、右鍵菜單事件等。
九、性能考慮
- 對于大型樹結構,建議實現(xiàn)虛擬滾動以提高性能
- 避免頻繁更新整個樹數(shù)據(jù),盡量局部更新
- 可以考慮添加節(jié)點懶加載功能,減少初始渲染壓力
到此這篇關于Vue3中Tree樹組件開發(fā)文檔的文章就介紹到這了,更多相關Vue3 Tree樹組件開發(fā)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
基于腳手架創(chuàng)建Vue項目實現(xiàn)步驟詳解
這篇文章主要介紹了基于腳手架創(chuàng)建Vue項目實現(xiàn)步驟詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-08-08
淺析前端路由簡介以及vue-router實現(xiàn)原理
路由就是用來跟后端服務器進行交互的一種方式,通過不同的路徑,來請求不同的資源,請求不同的頁面是路由的其中一種功能。這篇文章主要介紹了前端路由簡介以及vue-router實現(xiàn)原理,需要的朋友可以參考下2018-06-06

