refactor(tree): 树组件重构完成

This commit is contained in:
落小梅 2021-10-19 18:22:46 +08:00
parent f8a8d865bf
commit 11dc8f1e21
8 changed files with 276 additions and 178 deletions

View File

@ -8,6 +8,7 @@
:showCheckbox="showCheckbox" :showCheckbox="showCheckbox"
v-model:checkedKeys="checkedKeys" v-model:checkedKeys="checkedKeys"
@node-click="handleClick" @node-click="handleClick"
:disabled="disabled"
> >
</lay-tree> </lay-tree>
<br/> <br/>
@ -32,6 +33,7 @@
<pre> <pre>
{{ clickNode }} {{ clickNode }}
</pre> </pre>
<br/>
</template> </template>
<script setup> <script setup>
@ -202,6 +204,7 @@ const showLine = ref(true);
const clickNode = ref(null); const clickNode = ref(null);
const showCheckbox = ref(true); const showCheckbox = ref(true);
const checkedKeys = ref([1]); const checkedKeys = ref([1]);
const disabled = ref(false);
function handleClick(node) { function handleClick(node) {
clickNode.value = node clickNode.value = node
@ -216,12 +219,22 @@ function handleClick(node) {
| Name | Description | Accepted Values | | Name | Description | Accepted Values |
| -------- | ---- | ----------------------- | | -------- | ---- | ----------------------- |
| name | 原始属性 name | -- | | data | 树型组件数据,类型 TreeData \| TreeData[] | null |
| data | 树型组件数据,类型TreeData[] | null |
| showCheckbox | 是否显示复选框 | false | | showCheckbox | 是否显示复选框 | false |
| onlyIconControl | 是否仅允许节点左侧图标控制展开收缩 | false | | onlyIconControl | 是否仅允许节点左侧图标控制展开收缩 | false |
| showLine | 是否开启连接线 | true | | showLine | 是否开启连接线 | true |
| checkedKeys(v-model:checkedKeys) | 开启选中后选中的节点 | - | | checkedKeys(v-model:checkedKeys) | 开启showCheckbox后, 选中的节点 | [] |
::: field TreeData
:::
| Name | Description | Accepted Values |
| -------- | ---- | ----------------------- |
| id | 唯一值 | - |
| title | 节点名称 | - |
| children | 子节点 | [] |
| disabled | 该节点是否禁用 | false |
| spread | 该节点是否展开 | false | - |
::: field tree events ::: field tree events

View File

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

View File

@ -11,6 +11,7 @@ import { Nullable } from '/@src/module/type'
import LayIcon from '../../icon' import LayIcon from '../../icon'
import LayCheckbox from '../../checkbox' import LayCheckbox from '../../checkbox'
import { nextTick, Ref } from 'vue' import { nextTick, Ref } from 'vue'
import { Tree } from "/@src/module/tree/new-tree/tree";
type CustomKey = string | number type CustomKey = string | number
type CustomString = (() => string) | string type CustomString = (() => string) | string
@ -20,7 +21,7 @@ interface TreeData {
title: CustomString title: CustomString
children: TreeData[] children: TreeData[]
parentKey: Nullable<StringOrNumber> parentKey: Nullable<StringOrNumber>
isRoot: Ref<boolean> isRoot: boolean
isChecked: Ref<boolean> isChecked: Ref<boolean>
isDisabled: Ref<boolean> isDisabled: Ref<boolean>
isLeaf: Ref<boolean> isLeaf: Ref<boolean>
@ -29,13 +30,15 @@ interface TreeData {
} }
interface TreeNodeProps { interface TreeNodeProps {
tree: Tree
nodeList: TreeData[] nodeList: TreeData[]
showCheckbox: boolean showCheckbox: boolean
showLine: boolean showLine: boolean
onlyIconControl: boolean
} }
interface TreeNodeEmits { interface TreeNodeEmits {
(e: 'node-click', event: Event): void (e: 'node-click', node: TreeData): void
} }
const props = defineProps<TreeNodeProps>() const props = defineProps<TreeNodeProps>()
@ -68,60 +71,26 @@ const nodeIconType = (node: TreeData): string => {
return 'layui-icon-file' return 'layui-icon-file'
} }
function handleNodeClick(
args: { label: string; value: string },
node: TreeData
) {
emit('node-click', args, node)
}
function recursiveNodeClick( function recursiveNodeClick(
args: { label: string; value: string },
node: TreeData node: TreeData
) { ) {
emit('node-click', args, node) emit('node-click', 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)
}
}
function setParentChecked(checked: boolean, parent: TreeData) {
if (!parent) {
return
}
parent.isChecked.value = checked
const pChild = parent.children
const pChildChecked = pChild.some((c) => c.isChecked.value)
if (pChildChecked) {
parent.isChecked.value = true
}
if (parent.parentNode) {
setParentChecked(checked, parent.parentNode)
}
} }
function handleChange(checked: boolean, node: TreeData) { function handleChange(checked: boolean, node: TreeData) {
node.isChecked.value = checked props.tree.setCheckedKeys(checked, node)
//
if (node.parentNode) {
setParentChecked(checked, node.parentNode)
}
//
if (node.children) {
setChildrenChecked(checked, node.children)
}
} }
function handleIconClick(node: TreeData) { function handleIconClick(node: TreeData) {
node.isLeaf.value = !node.isLeaf.value node.isLeaf.value = !node.isLeaf.value
} }
function handleTitleClick(node: TreeData) {
if (!props.onlyIconControl) {
handleIconClick(node)
}
emit('node-click', node)
}
</script> </script>
<template> <template>
@ -161,6 +130,7 @@ function handleIconClick(node: TreeData) {
'layui-tree-txt': true, 'layui-tree-txt': true,
'layui-disabled': node.isDisabled.value, 'layui-disabled': node.isDisabled.value,
}" }"
@click="handleTitleClick(node)"
> >
{{ node.title }} {{ node.title }}
</span> </span>
@ -177,6 +147,8 @@ function handleIconClick(node: TreeData) {
:show-checkbox="showCheckbox" :show-checkbox="showCheckbox"
:show-line="showLine" :show-line="showLine"
@node-click="recursiveNodeClick" @node-click="recursiveNodeClick"
:tree="tree"
:only-icon-control="onlyIconControl"
/> />
</div> </div>
</transition> </transition>

View File

@ -15,6 +15,7 @@ export default {
import TreeNode from './TreeNode.vue' import TreeNode from './TreeNode.vue'
import { computed } from 'vue' import { computed } from 'vue'
import { useTree } from '/@src/module/tree/new-tree/useTree' import { useTree } from '/@src/module/tree/new-tree/useTree'
import { TreeData } from '/@src/module/tree/new-tree/tree'
type StringFn = () => string type StringFn = () => string
type StringOrNumber = string | number type StringOrNumber = string | number
@ -37,6 +38,7 @@ interface TreeProps {
accordion?: boolean accordion?: boolean
onlyIconControl?: boolean onlyIconControl?: boolean
showLine?: boolean showLine?: boolean
disabled?: boolean
replaceFields?: { replaceFields?: {
id?: string id?: string
children?: string children?: string
@ -46,7 +48,8 @@ interface TreeProps {
interface TreeEmits { interface TreeEmits {
(e: 'update:checkedKeys', keys: KeysType): void (e: 'update:checkedKeys', keys: KeysType): void
(e: 'node-click', args: any, node: OriginalTreeData, event: Event): void (e: 'update:expandKeys', keys: KeysType): void
(e: 'node-click', node: OriginalTreeData): void
} }
const props = withDefaults(defineProps<TreeProps>(), { const props = withDefaults(defineProps<TreeProps>(), {
@ -54,6 +57,7 @@ const props = withDefaults(defineProps<TreeProps>(), {
edit: false, edit: false,
accordion: false, accordion: false,
onlyIconControl: false, onlyIconControl: false,
disabled: false,
showLine: true, showLine: true,
replaceFields: () => { replaceFields: () => {
return { return {
@ -63,6 +67,7 @@ const props = withDefaults(defineProps<TreeProps>(), {
} }
}, },
}) })
const emit = defineEmits<TreeEmits>() const emit = defineEmits<TreeEmits>()
const className = computed(() => { const className = computed(() => {
@ -73,21 +78,21 @@ const className = computed(() => {
} }
}) })
const { const { tree, nodeList } = useTree(props, emit)
nodeList,
handleCheckbox
} = useTree(props, emit)
function handleClick (args, node) { function handleClick(node: TreeData) {
handleCheckbox(node) const originNode = tree.getOriginData(node.id)
emit('node-click', originNode)
} }
</script> </script>
<template> <template>
<div :class="className"> <div :class="className">
<tree-node <tree-node
:nodeList="nodeList" :tree="tree"
:showCheckbox="showCheckbox" :node-list="nodeList"
:showLine="showLine" :show-checkbox="showCheckbox"
:show-line="showLine"
:only-icon-control="onlyIconControl"
@node-click="handleClick" @node-click="handleClick"
></tree-node> ></tree-node>
</div> </div>

View File

@ -28,82 +28,16 @@ interface ReplaceFields {
} }
interface TreeConfig { interface TreeConfig {
disabled: boolean
showCheckbox: boolean showCheckbox: boolean
checkedKeys: StringOrNumber[] checkedKeys: StringOrNumber[]
expandKeys: StringOrNumber[]
nodeMap: Map<StringOrNumber, TreeData> nodeMap: Map<StringOrNumber, TreeData>
originMap: Map<StringOrNumber, OriginalTreeData>
replaceFields: ReplaceFields 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)
if (parent) {
console.log(parent.isChecked.value)
}
// 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 { class Tree {
private readonly config: TreeConfig protected config: TreeConfig
protected treeData: TreeData[] protected treeData: TreeData[]
constructor( constructor(
@ -111,7 +45,13 @@ class Tree {
origin: OriginalTreeData | OriginalTreeData[] origin: OriginalTreeData | OriginalTreeData[]
) { ) {
this.config = config this.config = config
this.treeData = this.createTree(origin) this.treeData = []
this.init(origin)
}
init(origin: OriginalTreeData | OriginalTreeData[]): void {
const tree = this.createTree(origin)
this.treeData = tree
} }
createTree( createTree(
@ -120,15 +60,7 @@ class Tree {
): TreeData[] { ): TreeData[] {
let data let data
if (!Array.isArray(origin)) { if (!Array.isArray(origin)) {
data = Array.of( data = Array.of(Object.assign({}, origin))
Object.assign({}, origin, {
// isRoot: true,
// isChecked: ref(false),
// isExpand: ref(false),
// isDisabled: ref(false),
// isLeaf: ref(false),
})
)
} else { } else {
data = origin data = origin
} }
@ -137,7 +69,7 @@ class Tree {
const len = data.length const len = data.length
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
const node = getNode(this.config, data[i], parentKey, i < len - 1) const node = this.getNode(data[i], parentKey, i < len - 1)
const nodeChildren = Reflect.get(node, children) const nodeChildren = Reflect.get(node, children)
const nodeHasChildren = !!Reflect.get(node, children) const nodeHasChildren = !!Reflect.get(node, children)
@ -150,13 +82,117 @@ class Tree {
return nodeList return nodeList
} }
getNode(
origin: OriginalTreeData,
parentKey: StringOrNumber,
hasNextSibling: boolean
): TreeData {
const {
nodeMap,
originMap,
checkedKeys,
expandKeys,
replaceFields: { children, id, title },
} = this.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 parentNode = nodeMap.get(parentKey)
const node = Object.assign({}, origin, {
id: nodeKey,
title: nodeTitle,
children: nodeChildren ? nodeChildren : [],
parentKey: parentKey,
isRoot: parentKey === '',
isDisabled: ref(false),
isChecked: ref(false),
isLeaf: ref(false),
hasNextSibling: hasNextSibling,
parentNode: parentNode || null,
})
node.isDisabled.value = nodeDisabled
node.isChecked.value = parentNode ? parentNode.isChecked.value : checkedKeys.includes(nodeKey)
node.isLeaf.value = parentNode ? parentNode.isLeaf.value : expandKeys.includes(nodeKey)
node.isLeaf.value = nodeIsLeaf
if (!nodeMap.has(nodeKey)) {
nodeMap.set(nodeKey, node)
}
if (!originMap.has(nodeKey)) {
originMap.set(nodeKey, origin)
}
return node
}
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 &&
this.setChildrenChecked(checked, nodes[i].children)
}
}
setParentChecked(checked: boolean, parent: TreeData) {
if (!parent) {
return
}
parent.isChecked.value = checked
const pChild = parent.children
const pChildChecked = pChild.some((c) => c.isChecked.value)
if (pChildChecked) {
parent.isChecked.value = true
}
if (parent.parentNode) {
this.setParentChecked(checked, parent.parentNode)
}
}
setCheckedKeys(checked: boolean, node: TreeData) {
node.isChecked.value = checked
// 处理上级
if (node.parentNode) {
this.setParentChecked(checked, node.parentNode)
}
// 处理下级
if (node.children) {
this.setChildrenChecked(checked, node.children)
}
}
getData() { getData() {
return this.treeData return this.treeData
} }
setChecked(node: TreeData) { getKeys () {
const item = this.config.nodeMap.get(node.id) const checkedKeys = []
console.log(item) const expandKeys = []
const iterator = this.config.nodeMap[Symbol.iterator]()
let next = iterator.next()
while (!next.done) {
const [, node] = next.value
const id = Reflect.get(node, this.config.replaceFields.id)
if (node.isChecked.value) {
checkedKeys.push(id)
}
if (node.isLeaf.value) {
expandKeys.push(id)
}
next = iterator.next()
}
return { checkedKeys, expandKeys }
}
getOriginData (key: StringOrNumber): OriginalTreeData {
return this.config.originMap.get(key)!
} }
} }

View File

@ -1,9 +1,3 @@
/*
* @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 StringFn = () => string
export type StringOrNumber = string | number export type StringOrNumber = string | number
export type KeysType = (number | string)[] export type KeysType = (number | string)[]
@ -19,6 +13,7 @@ export interface OriginalTreeData {
export interface TreeProps { export interface TreeProps {
checkedKeys?: KeysType checkedKeys?: KeysType
expandKeys?: KeysType
data: OriginalTreeData data: OriginalTreeData
showCheckbox?: boolean showCheckbox?: boolean
edit?: EditType edit?: EditType
@ -34,11 +29,7 @@ export interface TreeProps {
export interface TreeEmits { export interface TreeEmits {
(e: 'update:checkedKeys', keys: KeysType): void (e: 'update:checkedKeys', keys: KeysType): void
(e: 'update:expandKeys', keys: KeysType): void
(e: 'node-click', node: OriginalTreeData, event: Event): void (e: 'node-click', node: OriginalTreeData, event: Event): void
} }
/**
* Tree
*/
export interface Tree {}

View File

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

View File

@ -0,0 +1,83 @@
import {
OriginalTreeData, StringFn,
StringOrNumber
} from "/@src/module/tree/new-tree/tree.type";
import { computed, ref, Ref } from "vue";
import { Nullable } from "/@src/module/type";
export interface TreeConfig {
disabled: boolean
showLine: boolean
showCheckbox: boolean
spread: boolean
checkedKeys: StringOrNumber[]
spreadKeys: StringOrNumber[]
}
export const useTreeData = (
treeConfig: TreeConfig,
origin: OriginalTreeData | OriginalTreeData[]
) => {
const tree = computed(() => {
const initTree = initialTree(origin)
const treeNodes = getTreeNodes(initTree, treeConfig, '')
return treeNodes
})
}
function getTreeNodes(origin: OriginalTreeData[], config: TreeConfig, parentId = '') {
const len = origin.length
for (let i = 0; i < len; i++) {
const node = createTreeNode(config, origin[i], i, len, parentId)
}
}
function createTreeNode (config: TreeConfig, current: OriginalTreeData, i: number, len: number, parentId: string) {
const treeNode = new TreeNode(current)
const { disabled, spread } = config
treeNode.isDisabled.value = disabled
treeNode.isLeaf.value = spread
if (i < len - 1) {
treeNode.hasNextSibling = true
}
treeNode.parentId = parentId
}
class TreeNode {
id: StringOrNumber
parentId: StringOrNumber
title: StringFn | string
children: Nullable<TreeNode[]>
parentNode: Nullable<TreeNode>
isDisabled: Ref<boolean>
isLeaf: Ref<boolean>
isChecked: Ref<boolean>
hasNextSibling: boolean
constructor(options: OriginalTreeData) {
this.id = options.id
this.title = options.title
this.children = []
this.parentId = ''
this.parentNode = null
this.isDisabled = ref(false)
this.isLeaf = ref(false)
this.isChecked = ref(false)
this.hasNextSibling = false
}
}
/**
* init tree.
* if origin source is object then return Array.of(originSource)
* if array, return
* @param originData
*/
function initialTree(originData: OriginalTreeData | OriginalTreeData[]): any {
let treeNodes = []
if (!Array.isArray(originData)) {
treeNodes.push(originData)
} else {
treeNodes = Array.of(originData)
}
return treeNodes
}