From c8eec4babeefaaf6c40e0d071c3bca75e5c2e67e Mon Sep 17 00:00:00 2001 From: wangkai <1137120948@qq.com> Date: Sun, 21 Nov 2021 21:52:39 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20tree=E7=BB=84=E4=BB=B6=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=AD=90=E8=8A=82=E7=82=B9=EF=BC=8C=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E3=80=81=E5=88=A0=E9=99=A4=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tree/src/composables/use-operate.tsx | 60 ++++++ .../devui/tree/src/composables/use-toggle.ts | 13 +- .../devui/tree/src/tree-node-content.tsx | 37 +++- .../devui-vue/devui/tree/src/tree-types.ts | 7 +- packages/devui-vue/devui/tree/src/tree.tsx | 61 ++++-- packages/devui-vue/devui/tree/src/util.ts | 45 +++- .../devui-vue/docs/components/tree/index.md | 195 +++++++++++++++++- 7 files changed, 383 insertions(+), 35 deletions(-) create mode 100644 packages/devui-vue/devui/tree/src/composables/use-operate.tsx diff --git a/packages/devui-vue/devui/tree/src/composables/use-operate.tsx b/packages/devui-vue/devui/tree/src/composables/use-operate.tsx new file mode 100644 index 00000000..801f4a14 --- /dev/null +++ b/packages/devui-vue/devui/tree/src/composables/use-operate.tsx @@ -0,0 +1,60 @@ +import { ref, Ref } from 'vue' +import { TreeItem, TreeData } from '../tree-types' + +type TypeID = TreeItem['id'] +interface TypeOperator { + addable: boolean + editable: boolean + deletable: boolean + handleAdd: () => void + handleEdit: () => void + handleDelete: () => void +} +interface TypeOperateIconReflect { + id: TypeID, + renderIcon: (data: TreeItem) => JSX.Element +} +interface TypeeditStatusReflect { + [id: TypeID]: boolean +} +interface TypeReturnUseOperate { + editStatusReflect: Ref, + operateIconReflect: Ref>, + handleReflectIdToIcon: TypeHandleReflectIdToIcon, +} +type TypeUseOperate = (treeData: Ref) => TypeReturnUseOperate +type TypeHandleReflectIdToIcon = (id: TypeID, operate: TypeOperator) => void + +const reflectIconWithHandle = (data: TreeItem, operator: TypeOperator): JSX.Element => { + const handleAdd = (payload: MouseEvent) => operator.handleAdd() + const handleEdit = (payload: MouseEvent) => operator.handleEdit() + const handleDelete = (payload: MouseEvent) => operator.handleDelete() + return ( + <> + { operator.addable && } + { operator.editable && } + { operator.deletable && } + + ) +} + +const useOperate: TypeUseOperate = (treeData) => { + const operateIconReflect = ref>([]) + const editStatusReflect = ref({}) + + const handleReflectIdToIcon: TypeHandleReflectIdToIcon = (id, operator) => { + const isNotExistedReflectItem = operateIconReflect.value.every(({ id: d }) => d != id) + if (isNotExistedReflectItem) { + editStatusReflect.value[id] = false + operateIconReflect.value.push({ id, renderIcon: data => reflectIconWithHandle(data, operator) }) + } + } + + return { + operateIconReflect, + editStatusReflect, + handleReflectIdToIcon, + } +} + +export default useOperate diff --git a/packages/devui-vue/devui/tree/src/composables/use-toggle.ts b/packages/devui-vue/devui/tree/src/composables/use-toggle.ts index 632218d1..91711a69 100644 --- a/packages/devui-vue/devui/tree/src/composables/use-toggle.ts +++ b/packages/devui-vue/devui/tree/src/composables/use-toggle.ts @@ -1,6 +1,6 @@ -import { ref } from 'vue' +import { ref, Ref, watch } from 'vue' -export default function useToggle(data: unknown): any { +export default function useToggle(data: Ref): any { const openedTree = (tree) => { return tree.reduce((acc, item) => ( item.open @@ -9,13 +9,18 @@ export default function useToggle(data: unknown): any { ), []) } - const openedData = ref(openedTree(data)) + const openedData = ref(openedTree(data.value)) + watch( + () => data.value, + (d) => openedData.value = openedTree(d), + { deep: true } + ) const toggle = (target, item) => { target.stopPropagation() if (!item.children) return item.open = !item.open - openedData.value = openedTree(data) + openedData.value = openedTree(data.value) } return { diff --git a/packages/devui-vue/devui/tree/src/tree-node-content.tsx b/packages/devui-vue/devui/tree/src/tree-node-content.tsx index 3882736d..818a1717 100644 --- a/packages/devui-vue/devui/tree/src/tree-node-content.tsx +++ b/packages/devui-vue/devui/tree/src/tree-node-content.tsx @@ -1,4 +1,4 @@ -import { defineComponent, h, inject } from 'vue' +import { defineComponent, inject, toRefs, watch, onUpdated, ref } from 'vue' import { TreeRootType } from './tree-types' export default defineComponent({ @@ -8,15 +8,44 @@ export default defineComponent({ type: Object, required: true, }, + editStatusReflect: { + type: Object, + required: true, + }, }, setup(props) { const tree = inject('treeRoot') + + const getCurID = (id) => `devui-tree-node__input-${id}` + onUpdated(() => { + const target = document.querySelector(`#${getCurID(props.node.id)}`) as HTMLInputElement + target?.focus() + }) + return () => { - const node = props.node - const { disabled, label } = node + const { node, editStatusReflect } = toRefs(props) // 闭包 + const { disabled, label, id } = node.value // 闭包 + const handleChange = ({ target }) => { + node.value.label = target.value + } + const handleBlur = () => { + editStatusReflect.value[id] = false + } return tree.ctx.slots.default ? tree.ctx.slots.default({ node }) - : { label } + : + { + editStatusReflect.value[id] + ? + : label + } + } }, }) diff --git a/packages/devui-vue/devui/tree/src/tree-types.ts b/packages/devui-vue/devui/tree/src/tree-types.ts index 03c99401..58d6d70d 100644 --- a/packages/devui-vue/devui/tree/src/tree-types.ts +++ b/packages/devui-vue/devui/tree/src/tree-types.ts @@ -4,8 +4,11 @@ export interface TreeItem { id: string label: string isParent?: boolean - level: number + level?: number open?: boolean + addable?: boolean + editable?: boolean + deletable?: boolean children?: TreeData [key: string]: any } @@ -44,4 +47,4 @@ export type TreeProps = ExtractPropTypes export interface TreeRootType { ctx: SetupContext props: TreeProps -} \ No newline at end of file +} diff --git a/packages/devui-vue/devui/tree/src/tree.tsx b/packages/devui-vue/devui/tree/src/tree.tsx index f9b45da9..7239b04a 100644 --- a/packages/devui-vue/devui/tree/src/tree.tsx +++ b/packages/devui-vue/devui/tree/src/tree.tsx @@ -1,8 +1,8 @@ -import { defineComponent, toRefs, provide } from 'vue' +import { defineComponent, reactive, toRefs, provide } from 'vue' import type { SetupContext } from 'vue' import { treeProps, TreeProps, TreeItem, TreeRootType } from './tree-types' import { CHECK_CONFIG } from './config' -import { precheckTree } from './util' +import { preCheckTree, deleteNode, getId } from './util' import Loading from '../../loading/src/service' import Checkbox from '../../checkbox/src/checkbox' import useToggle from './composables/use-toggle' @@ -10,6 +10,7 @@ import useMergeNode from './composables/use-merge-node' import useHighlightNode from './composables/use-highlight' import useChecked from './composables/use-checked' import useLazy from './composables/use-lazy' +import useOperate from './composables/use-operate' import IconOpen from './assets/open.svg' import IconClose from './assets/close.svg' import NodeContent from './tree-node-content' @@ -20,20 +21,50 @@ export default defineComponent({ props: treeProps, emits: ['nodeSelected'], setup(props: TreeProps, ctx: SetupContext) { - const { data, checkable, checkableRelation: cbr } = toRefs({ ...props, data: precheckTree(props.data) }) + const { data, checkable, checkableRelation: cbr } = toRefs(reactive({ ...props, data: preCheckTree(props.data) })) const { mergeData } = useMergeNode(data.value) - const { openedData, toggle } = useToggle(mergeData.value) + const { openedData, toggle } = useToggle(mergeData) const { nodeClassNameReflect, handleInitNodeClassNameReflect, handleClickOnNode } = useHighlightNode() const { lazyNodesReflect, handleInitLazyNodeReflect, getLazyData } = useLazy() const { selected, onNodeClick } = useChecked(cbr, ctx, data.value) + const { editStatusReflect, operateIconReflect, handleReflectIdToIcon } = useOperate(data) provide('treeRoot', { ctx, props }) const Indent = () => { return } - const renderNode = (item: TreeItem) => { - const { id = '', label, disabled, open, isParent, level, children } = item + const { id = '', label, disabled, open, isParent, level, children, addable, editable, deletable } = item + handleReflectIdToIcon( + id, + { + addable, + editable, + deletable, + handleAdd: () => { + const newItem: TreeItem = { + id: getId(item.id), + label: 'new item', + level: item.level + 1, + addable, + editable, + deletable + } + item.open = true + if (item.children && Array.isArray(item.children)) { + item.children.push(newItem) + } else { + item.children = [newItem] + } + }, + handleEdit: () => { + editStatusReflect.value[id] = !editStatusReflect.value[id] + }, + handleDelete: () => { + mergeData.value = deleteNode(id, mergeData.value) + }, + } + ) handleInitNodeClassNameReflect(disabled, id) handleInitLazyNodeReflect(item, { id, @@ -63,7 +94,7 @@ export default defineComponent({ }) } }) - const renderNodeWithIcon = (item: TreeItem) => { + const renderFoldIcon = (item: TreeItem) => { const handleClick = async (target: MouseEvent) => { if (item.isParent) { item.children = await getLazyData(id) // item 按引用传递 @@ -71,7 +102,7 @@ export default defineComponent({ return toggle(target, item) } return ( - isParent || children + isParent || children && children.length ? open ? : @@ -89,16 +120,20 @@ export default defineComponent({ onClick={() => handleClickOnNode(id)} >
- {renderNodeWithIcon(item)} - {checkable.value && onNodeClick(item)} disabled={disabled} {...checkState} />} - - {item.isParent &&
} + { renderFoldIcon(item) } + { checkable.value && onNodeClick(item)} disabled={disabled} {...checkState} /> } + + { operateIconReflect.value.find(({ id: d }) => id === d).renderIcon(item) } + { + item.isParent + ?
+ : null + }
) } - const renderTree = (tree) => { return tree.map(item => { if (!item.children) { diff --git a/packages/devui-vue/devui/tree/src/util.ts b/packages/devui-vue/devui/tree/src/util.ts index 65c68355..548e5b94 100644 --- a/packages/devui-vue/devui/tree/src/util.ts +++ b/packages/devui-vue/devui/tree/src/util.ts @@ -1,5 +1,8 @@ +import { Ref } from 'vue' +import { random } from 'lodash' import { TreeData, TreeItem } from './tree-types' + export const omit = (obj: unknown, key: string): unknown => { return Object.entries(obj) .filter(item => item[0] !== key) @@ -16,23 +19,45 @@ export const flatten = (tree: Array, key = 'children'): Array => { * 用于设置 Tree Node 的 ID 属性 * 应用场景: 懒加载 loading 后元素定位 */ -const precheckNodeId = (d: TreeItem): TreeItem => { - const random = parseInt((Math.random() * (10 ** 8)).toString().padEnd(8, '0')) - return { ...d, id: d.id ? `${d.id}_${random}` : `${d.label.replaceAll(' ', '-')}_${random}` } +const getRandomId = (): string => random(10 ** 7, 10 ** 8 - 1).toString() +const preCheckNodeId = (d: TreeItem, postfixId?: string): TreeItem => { + const randomStr = getRandomId() + return { ...d, id: postfixId ? `${postfixId}_${randomStr}` : randomStr } +} +export const getId = (id: string): string => { + const ids = id.split('_') + return [...ids.slice(0, ids.length), getRandomId()].join('_') } /** * 用于 Tree Node 的数据格式检查 */ -export const precheckTree = (ds: TreeData): TreeData => { +export const preCheckTree = (ds: TreeData, postfixId?: string): TreeData => { return ds.map(d => { - const dd = precheckNodeId(d) - if (d.children) { - return { - ...dd, - children: precheckTree(d.children) + const dd = preCheckNodeId(d, postfixId) + return d.children ? { + ...dd, + children: preCheckTree(d.children, dd.id) + } : dd + }) +} + +const _deleteNode = (ids: Array, data: Array, index = 0): Array => { + const curTargetId = ids.slice(0, index + 2).join('_') + data.forEach(item => { + if (item.id === ids.slice(0, index + 1).join('_')) { + if (ids.length === index + 2) { + item.children = item.children.filter(({ id: d }) => d !== curTargetId) + } else { + item.children = _deleteNode(ids, item.children, index + 1) } } - return dd }) + return data +} +export const deleteNode = (id: string, data: Array): Array => { + if (id.includes('_')) { + return _deleteNode(id.split('_'), data) + } + return data.filter(({ id: d }) => d !== id) } diff --git a/packages/devui-vue/docs/components/tree/index.md b/packages/devui-vue/docs/components/tree/index.md index 3251f147..a3ab18fa 100644 --- a/packages/devui-vue/docs/components/tree/index.md +++ b/packages/devui-vue/docs/components/tree/index.md @@ -331,7 +331,7 @@ export default defineComponent({ status: "status1", children: [ { - label: "leaf node 1-1", + label: "leaf node 1-112121212", data: { type: "mix" }, open: false, status: "status1", @@ -738,4 +738,195 @@ export default defineComponent({ }) ``` -::: \ No newline at end of file +::: + +### 添加子节点,编辑、删除节点 + +:::demo 通过 checkableRelation 控制check时父子节点的表现。 + +```vue + + + +``` +::: -- Gitee From d4412dd7f4162767b933d09e32f02f9f7f10634d Mon Sep 17 00:00:00 2001 From: wangkai <1137120948@qq.com> Date: Tue, 30 Nov 2021 09:27:47 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E6=9B=BF=E6=8D=A2=20lodash.random?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/devui-vue/devui/tree/src/util.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/devui-vue/devui/tree/src/util.ts b/packages/devui-vue/devui/tree/src/util.ts index 548e5b94..9b0917e3 100644 --- a/packages/devui-vue/devui/tree/src/util.ts +++ b/packages/devui-vue/devui/tree/src/util.ts @@ -1,5 +1,3 @@ -import { Ref } from 'vue' -import { random } from 'lodash' import { TreeData, TreeItem } from './tree-types' @@ -19,9 +17,10 @@ export const flatten = (tree: Array, key = 'children'): Array => { * 用于设置 Tree Node 的 ID 属性 * 应用场景: 懒加载 loading 后元素定位 */ -const getRandomId = (): string => random(10 ** 7, 10 ** 8 - 1).toString() +const getRandomId = (): string => (Math.random() * 10 ** 9).toString().slice(0,8) const preCheckNodeId = (d: TreeItem, postfixId?: string): TreeItem => { const randomStr = getRandomId() + console.info('randomStr: ', randomStr) return { ...d, id: postfixId ? `${postfixId}_${randomStr}` : randomStr } } export const getId = (id: string): string => { -- Gitee