refactor(tree): 树组件重构提升性能

This commit is contained in:
落小梅 2021-10-18 00:55:58 +08:00
parent 6c0cb3041a
commit 6c96674cd1
10 changed files with 542 additions and 8 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

View File

@ -201,7 +201,7 @@ const iconCtrl = ref(false);
const showLine = ref(true);
const clickNode = ref(null);
const showCheckbox = ref(true);
const checkedKeys = ref([9, 10, 24]);
const checkedKeys = ref([1]);
function handleClick(node) {
clickNode.value = node
@ -215,9 +215,9 @@ function handleClick(node) {
:::
| Name | Description | Accepted Values |
| -------- | ---- | ----------------------- |
| name | 原始属性 name | -- |
| data | 树型组件数据,类型TreeData[] | null |
| -------- | ---- | ----------------------- |
| name | 原始属性 name | -- |
| data | 树型组件数据,类型TreeData[] | null |
| showCheckbox | 是否显示复选框 | false |
| onlyIconControl | 是否仅允许节点左侧图标控制展开收缩 | false |
| showLine | 是否开启连接线 | true |
@ -229,5 +229,5 @@ function handleClick(node) {
:::
| Name | Description | Accepted Params |
| -------- | ---- | ----------------------- |
| node-click | 节点 click 事件 | -- |
| -------- | ---- | ----------------------- |
| node-click | 节点 click 事件 | -- |

View File

@ -70,7 +70,7 @@ function handleNodeClick(node: TreeNode) {
* @param node
* @param type
*/
function innerClick(node: TreeNode, type: EventType) {
function innerClick(node: TreeNode, type: EventType, e?: Event) {
emit('node-click', node, type)
}

View File

@ -1,5 +1,6 @@
import type { App } from 'vue'
import Component from './index.vue'
// import Component from './new-tree/index.vue'
import type { IDefineComponent } from '../type/index'
Component.install = (app: App) => {

View File

@ -0,0 +1,170 @@
<script lang="ts">
export default {
name: 'TreeNode',
}
</script>
<script setup lang="ts">
import { StringOrNumber } from '/@src/module/tree/new-tree/tree.type'
import { Nullable } from '/@src/module/type'
import LayIcon from '../../icon'
import LayCheckbox from '../../checkbox'
import { nextTick, Ref } from 'vue'
type CustomKey = string | number
type CustomString = (() => string) | string
interface TreeData {
id: CustomKey
title: CustomString
children: TreeData[]
parentKey: Nullable<StringOrNumber>
isRoot: Ref<boolean>
isChecked: Ref<boolean>
isDisabled: Ref<boolean>
isLeaf: Ref<boolean>
hasNextSibling: boolean
parentNode: Nullable<TreeData>
}
interface TreeNodeProps {
nodeList: TreeData[]
showCheckbox: boolean
showLine: boolean
}
interface TreeNodeEmits {
(e: 'node-click', event: Event): void
}
const props = defineProps<TreeNodeProps>()
const emit = defineEmits<TreeNodeEmits>()
function renderLineShort(node: TreeData) {
return (
!node.hasNextSibling &&
node.parentKey &&
//
(!node.parentNode.hasNextSibling ||
//线
(node.parentNode.hasNextSibling && !node.parentNode.children))
)
}
/**
* 展开收起 icon样式
* @param node
*/
const nodeIconType = (node: TreeData): string => {
if (!props.showLine) {
if (node.children.length > 0) {
return 'layui-tree-iconArrow '
}
return ''
}
if (node.children.length !== 0) {
return !node.isLeaf.value ? 'layui-icon-addition' : 'layui-icon-subtraction'
}
return 'layui-icon-file'
}
function handleNodeClick (args: { label: string, value: string }, node: TreeData) {
emit('node-click', args, node)
}
function recursiveNodeClick (args: { label: string, value: string }, node: TreeData) {
emit('node-click', args, node)
}
function setChildrenChecked (checked: boolean, nodes: TreeData[]) {
const len = nodes.length
for (let i = 0; i < len; i++) {
nodes[i].isChecked.value = checked
nodes[i].children && nodes[i].children.length > 0 && (setChildrenChecked(checked, nodes[i].children))
}
}
async function setParentChecked (checked: boolean, parent: TreeData) {
if (!parent) {
return
}
parent.isChecked.value = checked
if (parent.parentNode) {
await setParentChecked(checked, parent.parentNode)
// todo
await nextTick()
const pChild = parent.children
const pChildChecked = pChild.some(c => c.isChecked.value)
console.log(pChildChecked)
if (pChildChecked) {
parent.isChecked.value = true
}
}
}
function handleChange(checked: boolean, node: TreeData) {
node.isChecked.value = checked
if (node.children) {
setChildrenChecked(checked, node.children)
}
setParentChecked(checked, node.parentNode)
}
function handleIconClick (node: TreeData) {
node.isLeaf.value = !node.isLeaf.value
}
</script>
<template>
<div
v-for="(node, nodeIndex) in nodeList"
:key="nodeIndex"
:class="{
'layui-tree-set': true,
'layui-tree-setLineShort': renderLineShort(node),
'layui-tree-setHide': node.isRoot,
}"
>
<div class="layui-tree-entry">
<div class="layui-tree-main">
<span
:class="[
showLine && node.children.length > 0 ? 'layui-tree-icon' : '',
{ 'layui-tree-iconClick': true },
]"
>
<LayIcon
:type="nodeIconType(node)"
@click="handleIconClick(node)"
/>
</span>
<LayCheckbox
v-if="showCheckbox"
v-model:checked="node.isChecked.value"
skin="primary"
@change="({ checked }) => { handleChange(checked, node) }"
>
</LayCheckbox>
<span class="layui-tree-txt">
{{ node.title }}
</span>
</div>
</div>
<transition>
<div
v-if="node.isLeaf.value"
class="layui-tree-pack layui-tree-showLine"
style="display: block"
>
<TreeNode
:node-list="node.children"
:show-checkbox="showCheckbox"
:show-line="showLine"
@node-click="recursiveNodeClick"
/>
</div>
</transition>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,94 @@
<!--
* @Date: 2021-10-16 13:22:38
* @LastEditors: 落小梅
* @LastEditTime: 2021-10-16 13:53:14
* @FilePath: \layui-vue\src\module\tree\new-tree\index.vue
-->
<script lang="ts">
export default {
name: 'LayTree',
}
// import { TreeEmits, TreeProps as Tp } from './tree.type'
</script>
<script lang="ts" setup>
import TreeNode from './TreeNode.vue'
import { computed } from 'vue'
import { useTree } from '/@src/module/tree/new-tree/useTree'
type StringFn = () => string
type StringOrNumber = string | number
type KeysType = (number | string)[]
type EditType = boolean | ('add' | 'update' | 'delete')
interface OriginalTreeData {
title: StringFn | string
id: StringOrNumber
field: StringFn | string
children?: OriginalTreeData[]
disabled?: boolean
}
interface TreeProps {
checkedKeys?: KeysType
data: OriginalTreeData
showCheckbox?: boolean
edit?: EditType
accordion?: boolean
onlyIconControl?: boolean
showLine?: boolean
replaceFields?: {
id?: string
children?: string
title?: string
}
}
interface TreeEmits {
(e: 'update:checkedKeys', keys: KeysType): void
(e: 'node-click', args: any, node: OriginalTreeData, event: Event): void
}
const props = withDefaults(defineProps<TreeProps>(), {
showCheckbox: false,
edit: false,
accordion: false,
onlyIconControl: false,
showLine: true,
replaceFields: () => {
return {
id: 'id',
children: 'children',
title: 'title',
}
},
})
const emit = defineEmits<TreeEmits>()
const className = computed(() => {
return {
'layui-tree': true,
'layui-form': props.showCheckbox,
'layui-tree-line': props.showLine,
}
})
const {
nodeList,
handleCheckbox
} = useTree(props, emit)
function handleClick (args, node) {
handleCheckbox(node)
}
</script>
<template>
<div :class="className">
<tree-node
:nodeList="nodeList"
:showCheckbox="showCheckbox"
:showLine="showLine"
@node-click="handleClick"
></tree-node>
</div>
</template>

View File

@ -0,0 +1,166 @@
/*
* @Date: 2021-10-16 02:50:17
* @LastEditors:
* @LastEditTime: 2021-10-16 13:07:14
* @FilePath: \layui-vue\src\module\tree\new-tree\tree.ts
*/
import {
OriginalTreeData,
StringOrNumber,
} from '/@src/module/tree/new-tree/tree.type'
import { Nullable } from '/@src/module/type'
import { Ref, ref } from 'vue'
type CustomKey = string | number
type CustomString = (() => string) | string
export interface TreeData {
id: CustomKey
title: CustomString
children: TreeData[]
parentKey: Nullable<StringOrNumber>
isRoot: boolean
isChecked: Ref<boolean>
isDisabled: Ref<boolean>
isLeaf: Ref<boolean>
hasNextSibling: boolean
parentNode: Nullable<TreeData>
}
interface ReplaceFields {
id: string
title: string
children: string
}
interface TreeConfig {
disabled: boolean
showCheckbox: boolean
checkedKeys: StringOrNumber[]
nodeMap: Map<StringOrNumber, TreeData>
replaceFields: ReplaceFields
}
function getNode(
config: TreeConfig,
origin: OriginalTreeData,
parentKey: StringOrNumber,
hasNextSibling: boolean
): TreeData {
const {
disabled,
checkedKeys,
showCheckbox,
nodeMap,
replaceFields: { children, id, title },
} = config
const nodeKey = Reflect.get(origin, id)
const nodeTitle = Reflect.get(origin, title)
const nodeChildren = Reflect.get(origin, children)
const nodeDisabled = !!Reflect.get(origin, 'disabled')
const nodeIsLeaf = !!Reflect.get(origin, 'spread')
// const parent = nodeMap.get(parentKey)
// console.log((parent && parent.isChecked.value) || checkedKeys.includes(nodeKey))
// const isCheckedValue = (parent && parent.isChecked.value) || checkedKeys.includes(nodeKey)
const node = Object.assign({}, origin, {
id: nodeKey,
title: nodeTitle,
children: nodeChildren ? nodeChildren : [],
parentKey: parentKey,
isRoot: parentKey === '',
isDisabled: ref(nodeDisabled),
isChecked: showCheckbox ? ref(checkedKeys.includes(nodeKey)) : ref(false),
// isChecked: ref(isCheckedValue),
isLeaf: ref(nodeIsLeaf),
hasNextSibling: hasNextSibling,
parentNode: null,
})
// 如果全局设置了disabled,则全部至为true
if (disabled) {
node.isDisabled.value = true
}
if (!nodeMap.has(nodeKey)) {
nodeMap.set(nodeKey, node)
}
return node
}
export function setParentNode(
nodes: TreeData[],
parentNode: Nullable<TreeData> = null
) {
const len = nodes.length
for (let i = 0; i < len; i++) {
Reflect.set(nodes[i], 'parentNode', parentNode)
if (nodes[i].children && nodes[i].children.length > 0) {
setParentNode(nodes[i].children, nodes[i])
}
}
}
class Tree {
private readonly config: TreeConfig
protected treeData: TreeData[]
constructor(
config: TreeConfig,
origin: OriginalTreeData | OriginalTreeData[]
) {
this.config = config
this.treeData = this.createTree(origin)
}
createTree(
origin: OriginalTreeData | OriginalTreeData[],
parentKey: StringOrNumber = ''
): TreeData[] {
let data
if (!Array.isArray(origin)) {
data = Array.of(
Object.assign({}, origin, {
// isRoot: true,
// isChecked: ref(false),
// isExpand: ref(false),
// isDisabled: ref(false),
// isLeaf: ref(false),
})
)
} else {
data = origin
}
const nodeList: TreeData[] = []
const { children } = this.config.replaceFields
const len = data.length
for (let i = 0; i < len; i++) {
const node = getNode(this.config, data[i], parentKey, i < len - 1)
const nodeChildren = Reflect.get(node, children)
const nodeHasChildren = !!Reflect.get(node, children)
if (nodeHasChildren) {
Reflect.set(node, children, this.createTree(nodeChildren, node.id))
}
nodeList.push(node)
}
return nodeList
}
getData() {
return this.treeData
}
setChecked(node: TreeData) {
const item = this.config.nodeMap.get(node.id)
console.log(item)
}
}
export { Tree }

View File

@ -0,0 +1,44 @@
/*
* @Date: 2021-10-16 13:07:34
* @LastEditors:
* @LastEditTime: 2021-10-16 13:29:06
* @FilePath: \layui-vue\src\module\tree\new-tree\tree.type.ts
*/
export type StringFn = () => string
export type StringOrNumber = string | number
export type KeysType = (number | string)[]
export type EditType = boolean | ('add' | 'update' | 'delete')
export interface OriginalTreeData {
title: StringFn | string
id: StringOrNumber
field: StringFn | string
children?: OriginalTreeData[]
disabled?: boolean
}
export interface TreeProps {
checkedKeys?: KeysType
data: OriginalTreeData
showCheckbox?: boolean
edit?: EditType
accordion?: boolean
onlyIconControl?: boolean
showLine?: boolean
replaceFields?: {
id?: string
children?: string
title?: string
}
}
export interface TreeEmits {
(e: 'update:checkedKeys', keys: KeysType): void
(e: 'node-click', node: OriginalTreeData, event: Event): void
}
/**
* Tree
*/
export interface Tree {}

View File

@ -0,0 +1,51 @@
import { TreeEmits, TreeProps } from '/@src/module/tree/new-tree/tree.type'
import { computed, ComputedRef, watch } from 'vue'
import { setParentNode, Tree, TreeData } from '/@src/module/tree/new-tree/tree'
export declare type UseTree = (
props: TreeProps,
emit: TreeEmits
) => {
nodeList: ComputedRef<TreeData[]>
handleCheckbox: (node: TreeData) => void
}
export const useTree: UseTree = (props: TreeProps, emit: TreeEmits) => {
const tree = computed(() => {
return new Tree(
{
disabled: false,
nodeMap: new Map(),
replaceFields: {
id: 'id',
title: 'title',
children: 'children',
},
showCheckbox: props.showCheckbox ?? false,
checkedKeys: props.checkedKeys ?? [],
},
props.data
)
})
const nodeList = computed(() => {
const nodes = tree.value.getData()
setParentNode(nodes)
return nodes
})
watch(
() => nodeList,
(list) => {
},
{ deep: true }
)
function handleCheckbox(node: TreeData) {
tree.value.setChecked(node)
}
return {
nodeList,
handleCheckbox,
}
}

View File

@ -6,7 +6,6 @@ import { WritableComputedRef } from 'vue'
* parentId
* @param data
* @param parentId
* @param checkedKeys
*/
export const generatorTreeData = (
data: TreeData[] | TreeNode[],