feat(tree): 完成树形组件复选框功能

This commit is contained in:
落小梅 2021-10-14 16:37:17 +08:00
parent c559a2d74a
commit 7caf70281c
7 changed files with 307 additions and 213 deletions

View File

@ -60,6 +60,7 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off',
'vue/one-component-per-file': 'off', 'vue/one-component-per-file': 'off',
'@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-non-null-assertion': 'off',
'vue/no-mutating-props': 'off',
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
}, },

View File

@ -6,6 +6,7 @@
:onlyIconControl="iconCtrl" :onlyIconControl="iconCtrl"
:showLine="showLine" :showLine="showLine"
:showCheckbox="showCheckbox" :showCheckbox="showCheckbox"
v-model:checkedKeys="checkedKeys"
@node-click="handleClick" @node-click="handleClick"
> >
</lay-tree> </lay-tree>
@ -14,6 +15,10 @@
<br/> <br/>
<lay-switch v-model="showCheckbox"></lay-switch> <lay-switch v-model="showCheckbox"></lay-switch>
<br/> <br/>
checkedKeys
<pre>
{{ checkedKeys }}
</pre>
只能通过节点左侧图标来展开收缩: 只能通过节点左侧图标来展开收缩:
<br/> <br/>
<lay-switch v-model="iconCtrl"></lay-switch> <lay-switch v-model="iconCtrl"></lay-switch>
@ -25,207 +30,178 @@
当前点击的节点: 当前点击的节点:
<br/> <br/>
<pre> <pre>
{{ clickNode }} {{ clickNode }}
</pre> </pre>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
const data = ref([ const data = ref([{
{ title: '一级1',
title: '一级1', id: 1,
id: 1, field: 'name1',
field: 'name1', checked: true,
checked: true, spread: true,
spread: true, children: [{
children: [ title: '二级1-1 可允许跳转',
{ id: 3,
title: '二级1-1 可允许跳转', field: 'name11',
id: 3, href: 'https://www.layui.com/',
field: 'name11', children: [{
href: 'https://www.layui.com/', title: '三级1-1-3',
children: [ id: 23,
{ field: '',
title: '三级1-1-3', children: [{
id: 23, title: '四级1-1-3-1',
field: '', id: 24,
children: [ field: '',
{ children: [{
title: '四级1-1-3-1', title: '五级1-1-3-1-1',
id: 24, id: 30,
field: '', field: ''
children: [ },
{ {
title: '五级1-1-3-1-1', title: '五级1-1-3-1-2',
id: 30, id: 31,
field: '', field: ''
}, }]
{ }]
title: '五级1-1-3-1-2', },
id: 31, {
field: '', title: '三级1-1-1',
}, id: 7,
], field: '',
}, children: [{
], title: '四级1-1-1-1 可允许跳转',
}, id: 15,
{ field: '',
title: '三级1-1-1', href: 'https://www.layui.com/doc/'
id: 7, }]
field: '', },
children: [ {
{ title: '三级1-1-2',
title: '四级1-1-1-1 可允许跳转', id: 8,
id: 15, field: '',
field: '', children: [{
href: 'https://www.layui.com/doc/', title: '四级1-1-2-1',
}, id: 32,
], field: ''
}, }]
{ }]
title: '三级1-1-2', },
id: 8, {
field: '', title: '二级1-2',
children: [ id: 4,
{ spread: true,
title: '四级1-1-2-1', children: [{
id: 32, title: '三级1-2-1',
field: '', id: 9,
}, field: '',
], disabled: true
}, },
], {
}, title: '三级1-2-2',
{ id: 10,
title: '二级1-2', field: ''
id: 4, }]
spread: true, },
children: [ {
{ title: '二级1-3',
title: '三级1-2-1', id: 20,
id: 9, field: '',
field: '', children: [{
disabled: true, title: '三级1-3-1',
}, id: 21,
{ field: ''
title: '三级1-2-2', },
id: 10, {
field: '', title: '三级1-3-2',
}, id: 22,
], field: ''
}, }]
{ }]
title: '二级1-3', },
id: 20, {
field: '', title: '一级2',
children: [ id: 2,
{ field: '',
title: '三级1-3-1', spread: true,
id: 21, children: [{
field: '', title: '二级2-1',
}, id: 5,
{ field: '',
title: '三级1-3-2', spread: true,
id: 22, children: [{
field: '', title: '三级2-1-1',
}, id: 11,
], field: ''
}, },
], {
}, title: '三级2-1-2',
{ id: 12,
title: '一级2', field: ''
id: 2, }]
field: '', },
spread: true, {
children: [ title: '二级2-2',
{ id: 6,
title: '二级2-1', field: '',
id: 5, children: [{
field: '', title: '三级2-2-1',
spread: true, id: 13,
children: [ field: ''
{ },
title: '三级2-1-1', {
id: 11, title: '三级2-2-2',
field: '', id: 14,
}, field: '',
{ disabled: true
title: '三级2-1-2', }]
id: 12, }]
field: '', },
}, {
], title: '一级3',
}, id: 16,
{ field: '',
title: '二级2-2', children: [{
id: 6, title: '二级3-1',
field: '', id: 17,
children: [ field: '',
{ fixed: true,
title: '三级2-2-1', children: [{
id: 13, title: '三级3-1-1',
field: '', id: 18,
}, field: ''
{ },
title: '三级2-2-2', {
id: 14, title: '三级3-1-2',
field: '', id: 19,
disabled: true, field: ''
}, }]
], },
}, {
], title: '二级3-2',
}, id: 27,
{ field: '',
title: '一级3', children: [{
id: 16, title: '三级3-2-1',
field: '', id: 28,
children: [ field: ''
{ },
title: '二级3-1', {
id: 17, title: '三级3-2-2',
field: '', id: 29,
fixed: true, field: ''
children: [ }]
{ }]
title: '三级3-1-1', }]);
id: 18,
field: '',
},
{
title: '三级3-1-2',
id: 19,
field: '',
},
],
},
{
title: '二级3-2',
id: 27,
field: '',
children: [
{
title: '三级3-2-1',
id: 28,
field: '',
},
{
title: '三级3-2-2',
id: 29,
field: '',
},
],
},
],
},
]);
const iconCtrl = ref(false); const iconCtrl = ref(false);
const showLine = ref(true); const showLine = ref(true);
const clickNode = ref(null); const clickNode = ref(null);
const showCheckbox = ref(true); const showCheckbox = ref(true);
const checkedKeys = ref([9, 10, 24]);
function handleClick(node) { function handleClick(node) {
clickNode.value = node clickNode.value = node

View File

@ -12,7 +12,8 @@ type EventType = 'icon' | 'node'
interface TreeEntityProps { interface TreeEntityProps {
node: TreeNode node: TreeNode
showCheckbox?: boolean showCheckbox?: boolean,
updateCheckedByNode: (node: TreeNode) => void
} }
interface EmitEvent { interface EmitEvent {
@ -73,7 +74,14 @@ function innerClick(node: TreeNode, type: EventType) {
emit('node-click', node, type) emit('node-click', node, type)
} }
console.log(props.showCheckbox) /**
* checkbox click
* @param arg
* @param node
*/
function handleCheckboxChange (arg: { checked: boolean, value: string }, node: TreeNode) {
props.updateCheckedByNode(node)
}
</script> </script>
<template> <template>
<template v-if="node.children && node.children.length > 0"> <template v-if="node.children && node.children.length > 0">
@ -96,13 +104,12 @@ console.log(props.showCheckbox)
v-if="showCheckbox" v-if="showCheckbox"
name="name" name="name"
skin="primary" skin="primary"
label="1" v-model:checked="node._checked"
:checked="true" @change="(args) => { handleCheckboxChange(args, node) }"
> >
{{ node.title }} <!-- {{ node.title }} || {{node.id}}-->
</LayCheckbox> </LayCheckbox>
<span <span
v-else
class="layui-tree-txt" class="layui-tree-txt"
@click.prevent.stop="handleNodeClick(node, 'node')" @click.prevent.stop="handleNodeClick(node, 'node')"
> >
@ -119,8 +126,9 @@ console.log(props.showCheckbox)
v-for="(item, index) in node.children" v-for="(item, index) in node.children"
:key="index" :key="index"
:node="item" :node="item"
:show-checkbox="showCheckbox"
@node-click="innerClick" @node-click="innerClick"
:showCheckbox="showCheckbox" :updateCheckedByNode="updateCheckedByNode"
/> />
</div> </div>
</div> </div>
@ -145,12 +153,11 @@ console.log(props.showCheckbox)
name="name" name="name"
skin="primary" skin="primary"
label="1" label="1"
:checked="true" v-model:checked="node._checked"
@change="(args) => { handleCheckboxChange(args, node) }"
> >
{{ node.title }}
</LayCheckbox> </LayCheckbox>
<span <span
v-else
class="layui-tree-txt" class="layui-tree-txt"
@click.prevent.stop="handleNodeClick(node, 'node')" @click.prevent.stop="handleNodeClick(node, 'node')"
> >

View File

@ -163,10 +163,12 @@ const props = withDefaults(defineProps<TreeProps>(), {
const emit = defineEmits<TreeEmits>() const emit = defineEmits<TreeEmits>()
const { innerTreeData, updateInnerTreeData, treeWrapperClass } = useTreeData( const {
props, innerTreeData,
emit updateInnerTreeData,
) treeWrapperClass,
updateCheckedByNode,
} = useTreeData(props, emit)
function handleNodeClick(node: TreeNode, type: 'node' | 'icon') { function handleNodeClick(node: TreeNode, type: 'node' | 'icon') {
// icon // icon
@ -191,8 +193,9 @@ export default {
v-for="(node, index) in innerTreeData" v-for="(node, index) in innerTreeData"
:key="node.id || index" :key="node.id || index"
:node="node" :node="node"
:show-checkbox="showCheckbox"
@node-click="handleNodeClick" @node-click="handleNodeClick"
:showCheckbox="showCheckbox" :updateCheckedByNode="updateCheckedByNode"
/> />
</div> </div>
</template> </template>

View File

@ -176,6 +176,7 @@ export interface TreeNode extends TreeData {
_parentNode?: Nullable<TreeNode> _parentNode?: Nullable<TreeNode>
_nextSibling?: Nullable<TreeNode> _nextSibling?: Nullable<TreeNode>
_expand?: boolean _expand?: boolean
_checked?: boolean
} }
/** hook type **/ /** hook type **/
@ -188,4 +189,5 @@ export type UseTreeData = (
spreadKeys: WritableComputedRef<(string | number)[]> spreadKeys: WritableComputedRef<(string | number)[]>
treeWrapperClass: ComputedRef<Recordable> treeWrapperClass: ComputedRef<Recordable>
updateInnerTreeData: (treeData: TreeData[], node: TreeData) => void updateInnerTreeData: (treeData: TreeData[], node: TreeData) => void
updateCheckedByNode: (treeNode: TreeNode) => void
} }

View File

@ -1,6 +1,6 @@
import { TreeData, TreeNode } from '/@src/module/tree/tree.type' import { TreeData, TreeNode } from '/@src/module/tree/tree.type'
import { Nullable } from '/@src/module/type' import { Nullable } from '/@src/module/type'
import { WritableComputedRef } from 'vue' import { Ref, WritableComputedRef } from "vue";
/** /**
* parentId * parentId
@ -10,8 +10,7 @@ import { WritableComputedRef } from 'vue'
*/ */
export const generatorTreeData = ( export const generatorTreeData = (
data: TreeData[] | TreeNode[], data: TreeData[] | TreeNode[],
parentId: TreeNode['parentId'] = '', parentId: TreeNode['parentId'] = ''
checkedKeys: WritableComputedRef<(string | number)[]>
): TreeNode[] => { ): TreeNode[] => {
const innerTreeData: TreeNode[] = [] const innerTreeData: TreeNode[] = []
const len = data.length const len = data.length
@ -20,10 +19,10 @@ export const generatorTreeData = (
const inner = { const inner = {
...item, ...item,
parentId: parentId, parentId: parentId,
spread: item.spread || false, spread: item.spread || false
} }
if (item.children && item.children.length > 0) { if (item.children && item.children.length > 0) {
inner.children = generatorTreeData(item.children, item.id, checkedKeys) inner.children = generatorTreeData(item.children, item.id)
} }
innerTreeData.push(inner as TreeNode) innerTreeData.push(inner as TreeNode)
} }
@ -71,9 +70,10 @@ export const initialTreeData = (
data: TreeData[], data: TreeData[],
checkedKeys: WritableComputedRef<(string | number)[]> checkedKeys: WritableComputedRef<(string | number)[]>
): TreeNode[] => { ): TreeNode[] => {
const innerTree = generatorTreeData(data, '', checkedKeys) const innerTree = generatorTreeData(data, '')
setNextSiblings(innerTree) setNextSiblings(innerTree)
setParentNode(innerTree) setParentNode(innerTree)
patchCheckedKeys(innerTree, checkedKeys)
return innerTree return innerTree
} }
@ -123,3 +123,88 @@ export const getEmitNode = (
} }
return item return item
} }
/**
* checkedKes分发到node
* @param tree
* @param checkedKeys
* @param checked
*/
export const patchCheckedKeys = (tree: TreeNode[], checkedKeys: WritableComputedRef<(string | number)[]>, checked = false): void => {
if (!checkedKeys.value) {
return
}
const len = tree.length
for (let i = 0; i < len; i++){
tree[i]._checked = checked
const node = tree[i]
// 该节点是checked
if (checkedKeys.value.indexOf(node.id) > -1) {
node._checked = true
if (node.children && node.children.length > 0) {
patchCheckedKeys(node.children, checkedKeys, true)
}
} else {
if (node.children && node.children.length > 0) {
patchCheckedKeys(node.children, checkedKeys, false)
// 判断children是否为都选中如果是都选中的情况下父组件也得是选中
const allChildrenChecked = node.children.every(it => it._checked)
if (allChildrenChecked) {
node._checked = true
}
}
}
}
}
function updateChildren(data: TreeNode[], flag: boolean) {
for (let i = 0; i < data.length; i++) {
data[i]._checked = flag
if (data[i].children && data[i].children.length > 0) {
updateChildren(data[i].children, flag)
}
}
}
/**
*
* @param data
* @param clickNode
* @param parentNode
*/
export function updateInnerTreeDataChecked(data: TreeNode[], clickNode: TreeNode, parentNode?: TreeNode) {
const len = data.length
for (let i = 0; i < len; i++) {
const currentNode = data[i]
// 找到当前更新的节点
if (currentNode.id === clickNode.id) {
// 如果当前节点有子节点,更新子节点
if (currentNode.children && currentNode.children.length > 0) {
updateChildren(data[i].children, currentNode._checked!)
}
} else {
if (currentNode.children && currentNode.children.length > 0) {
updateInnerTreeDataChecked(currentNode.children, clickNode, currentNode)
}
}
// 当前节点有选中,父节点一定选中
if (currentNode.children && currentNode.children.length > 0) {
currentNode._checked = currentNode.children.some(it => it._checked)
}
}
}
export function getCheckedKeys(tree: TreeNode[]): (string | number)[] {
let keys: (string | number) [] = []
const len = tree.length
for (let i = 0; i < len; i++) {
const current = tree[i]
if (current._checked) {
keys.push(current.id)
if (current.children && current.children.length > 0) {
keys = [...keys, ...getCheckedKeys(current.children)]
}
}
}
return keys
}

View File

@ -1,9 +1,10 @@
import { TreeData, TreeNode, UseTreeData } from '/@src/module/tree/tree.type' import { TreeData, TreeNode, UseTreeData } from '/@src/module/tree/tree.type'
import { computed, ref, unref, watch } from 'vue' import { computed, ref, unref, watch } from 'vue'
import { import {
getCheckedKeys,
getTreeSpreadKeys, getTreeSpreadKeys,
initialTreeData, initialTreeData, updateInnerTreeDataChecked
} from '/@src/module/tree/treeHelper' } from "/@src/module/tree/treeHelper";
import { Recordable } from '/@src/module/type' import { Recordable } from '/@src/module/type'
export const useTreeData: UseTreeData = (props, emit) => { export const useTreeData: UseTreeData = (props, emit) => {
@ -43,6 +44,16 @@ export const useTreeData: UseTreeData = (props, emit) => {
{ immediate: true, deep: true } { immediate: true, deep: true }
) )
/**
* watch innerTreeDate to emit checkedKeys
* @param treeData
* @param node
*/
watch(innerTreeData, tree => {
const emitCheckedKeys = getCheckedKeys(tree)
checkedKeys.value = emitCheckedKeys
}, { deep: true })
function updateInnerTreeData(treeData: TreeData[], node: TreeData): void { function updateInnerTreeData(treeData: TreeData[], node: TreeData): void {
for (let i = 0; i < treeData.length; i++) { for (let i = 0; i < treeData.length; i++) {
if (treeData[i].id === node.id) { if (treeData[i].id === node.id) {
@ -67,11 +78,20 @@ export const useTreeData: UseTreeData = (props, emit) => {
} }
}) })
/**
* checked状态到node中
* @param node
*/
function updateCheckedByNode (node: TreeNode) {
updateInnerTreeDataChecked(innerTreeData.value, node)
}
return { return {
spreadKeys, spreadKeys,
checkedKeys, checkedKeys,
innerTreeData, innerTreeData,
updateInnerTreeData, updateInnerTreeData,
treeWrapperClass, treeWrapperClass,
updateCheckedByNode
} }
} }