diff --git a/CHANGELOG.md b/CHANGELOG.md index d29e9a8f44759aaab7d4c7aaf5c9b6d814f3a843..7361f24ab3d6154e2c9f53599088534fbc38abae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Added - 新增识别全局应用参数contextMenuRightClickInvoke=false,拦截右键事件不弹出节点菜单 +- 树部件支持自定义树样式 ### Changed diff --git a/src/control/tree/el-tree-util.ts b/src/control/tree/el-tree-util.ts index dd632ca99d6a42011006efbb44be6d5185771de7..2a6009cb99c36293d9db3aa1363c3a2c5c6c5626 100644 --- a/src/control/tree/el-tree-util.ts +++ b/src/control/tree/el-tree-util.ts @@ -6,11 +6,12 @@ import { TreeController, } from '@ibiz-template/runtime'; import { IDETree } from '@ibiz/model-core'; +import { useNamespace } from '@ibiz-template/vue3-util'; import { ElTree } from 'element-plus'; import { createUUID } from 'qx-util'; import { NodeDropType } from 'element-plus/es/components/tree/src/tree.type'; import { cloneDeep, debounce } from 'lodash-es'; -import { Ref, watch } from 'vue'; +import { nextTick, Ref, watch } from 'vue'; /** * 树props接口 @@ -78,6 +79,100 @@ export function useElTreeUtil( return elTree; }; + const ns = useNamespace(`control-${c.model.controlType!.toLowerCase()}`); + let rootExpandKeys: string[] = []; + + // 最小折叠高度 + const minCollpaseHeight = 120; + + const { treeStyle } = c.model; + + /** + * @description 设置节点高度,用户自定义模式时使用 + * @param {HTMLElement} elTree + * @param {string} key + * @param {boolean} [full=false] + */ + const setNodeHeight = ( + elTree: HTMLElement, + key: string, + full: boolean = false, + ) => { + const dom: HTMLElement | null = elTree.querySelector( + `.el-tree-node[data-key="${key}"]`, + ); + if (dom) { + dom.classList.add('is-transition'); + if (full) { + dom.style.setProperty(ns.cssVarBlockName('root-node-grow'), '1'); + dom.style.setProperty( + ns.cssVarBlockName('root-node-max-height'), + 'unset', + ); + dom.style.setProperty(ns.cssVarBlockName('root-node-height'), '0'); + } else { + const height = c.state.rootNodeHeight[key]; + dom.style.setProperty( + ns.cssVarBlockName('root-node-max-height'), + height ? `${height}px` : null, + ); + dom.style.setProperty(ns.cssVarBlockName('root-node-height'), 'auto'); + dom.style.setProperty(ns.cssVarBlockName('root-node-grow'), null); + } + nextTick(() => { + dom.classList.remove('is-transition'); + }); + } + }; + + /** + * @description 清除节点高度 + * @param {HTMLElement} elTree + * @param {string} key + */ + const clearNodeHeight = (elTree: HTMLElement, key: string) => { + const dom: HTMLElement | null = elTree.querySelector( + `.el-tree-node[data-key="${key}"]`, + ); + if (dom) { + dom.style.setProperty(ns.cssVarBlockName('root-node-height'), null); + dom.style.setProperty(ns.cssVarBlockName('root-node-max-height'), null); + dom.style.setProperty(ns.cssVarBlockName('root-node-grow'), null); + } + }; + + const calcNodeHeight = (expandKeys: string[]) => { + const elTree = treeRef.value; + if (!elTree) { + return; + } + // 用户自定义样式,设置展开高度 + const rootNodes = Object.values(elTree.store.nodesMap).filter( + node => node.level === 1, + ); + // 初始化根节点高度 + if (!Object.keys(c.state.rootNodeHeight).length) { + rootNodes.forEach(node => { + c.state.rootNodeHeight[node.key] = minCollpaseHeight; + }); + } + // 用户自定义样式,折叠高度 + rootNodes.forEach(node => { + const index = expandKeys.indexOf(node.key as string); + // 折叠 + if (index === -1) { + clearNodeHeight(elTree.$el, node.key as string); + } else { + // 最后展开节点全展开 + setNodeHeight( + elTree.$el, + node.key as string, + index === expandKeys.length - 1, + ); + } + }); + }; + const _updateUI = () => { const elTree = treeRef.value; if (!elTree) { @@ -86,6 +181,7 @@ export function useElTreeUtil( }, 200); return; } + rootExpandKeys = []; // 设置节点展开 Object.values(elTree.store.nodesMap).forEach(node => { const shouldExpanded = c.state.expandedKeys.includes(node.data._id); @@ -96,6 +192,10 @@ export function useElTreeUtil( node.collapse(); } } + // 用户自定义样式,折叠高度 + if (node.level === 1 && shouldExpanded) { + rootExpandKeys.push(node.key as string); + } }); // 设置选中状态。 if (c.state.singleSelect) { @@ -104,6 +204,87 @@ export function useElTreeUtil( // el-tree,会把没选中的反选,且不触发check事件 elTree.setCheckedKeys(c.state.selectedData.map(item => item._id)); } + if (treeStyle === 'USER') { + calcNodeHeight(c.state.expandedKeys); + } + }; + + // 分割线拖拽相关 + let splitPageY: number = 0; + let splitNodeKey = ''; + let prevNodeKey = ''; + + const setRootNodeHeight = ( + elTree: HTMLElement, + key: string, + height: number, + ) => { + const currentNodekey = + c.state.expandedKeys[c.state.expandedKeys.length - 1]; + if (currentNodekey === key) { + return; + } + c.state.rootNodeHeight[key] = height; + setNodeHeight(elTree, key); + }; + + /** + * @description 分割线拖拽 + * @param {MouseEvent} e + */ + const onSplitMouseMove = (e: MouseEvent) => { + const moveOffset = e.pageY - splitPageY; + splitPageY = e.pageY; + const elTree = treeRef.value!; + const currentNodekey = + c.state.expandedKeys[c.state.expandedKeys.length - 1]; + // 向上移动 + if (moveOffset < 0) { + const prevHeight = c.state.rootNodeHeight[prevNodeKey] + moveOffset; + const splitHeight = c.state.rootNodeHeight[splitNodeKey] - moveOffset; + if (prevHeight < 120 && prevNodeKey !== currentNodekey) { + return; + } + setRootNodeHeight(elTree.$el, prevNodeKey, prevHeight); + setRootNodeHeight(elTree.$el, splitNodeKey, splitHeight); + } + // 向下移动 + if (moveOffset > 0) { + const prevHeight = c.state.rootNodeHeight[prevNodeKey] + moveOffset; + const splitHeight = c.state.rootNodeHeight[splitNodeKey] - moveOffset; + if (splitHeight < 120 && splitNodeKey !== currentNodekey) { + return; + } + setRootNodeHeight(elTree.$el, prevNodeKey, prevHeight); + setRootNodeHeight(elTree.$el, splitNodeKey, splitHeight); + } + }; + + /** + * @description 分割线拖拽结束 + */ + const onSplitMouseUp = () => { + splitPageY = 0; + splitNodeKey = ''; + prevNodeKey = ''; + document.removeEventListener('mousemove', onSplitMouseMove); + document.removeEventListener('mouseup', onSplitMouseUp); + }; + + /** + * @description 分割线拖拽开始 + * @param {MouseEvent} event + * @param {IData} node + */ + const onSplitMousedown = (event: MouseEvent, node: IData) => { + event.stopPropagation(); + event.preventDefault(); + splitPageY = event.pageY; + splitNodeKey = node.key; + const index = rootExpandKeys.indexOf(splitNodeKey); + prevNodeKey = rootExpandKeys[index - 1]; + document.addEventListener('mousemove', onSplitMouseMove); + document.addEventListener('mouseup', onSplitMouseUp); }; /** @@ -134,7 +315,7 @@ export function useElTreeUtil( } }; - return { getTreeInstance, updateUI, triggerNodeExpand }; + return { getTreeInstance, updateUI, triggerNodeExpand, onSplitMousedown }; } /** diff --git a/src/control/tree/tree.scss b/src/control/tree/tree.scss index 80c974a250999a48f9077856bd48e4d7a85e688c..59630b5b8d295060f30a17223905e0031b912ef5 100644 --- a/src/control/tree/tree.scss +++ b/src/control/tree/tree.scss @@ -183,3 +183,52 @@ $control-treeview-tree: ( @include b(control-treeview-editing-node) { --el-input-height: #{getCssVar(control-treeview, node, height)}; } + +@include b(control-treeview-tree) { + @include m(user) { + display: flex; + flex-direction: column; + height: 100%; + #{getCssVarName(control-treeview, root-node-height)}: auto; + #{getCssVarName(control-treeview, root-node-max-height)}: unset; + + >.el-tree-node { + height: getCssVar(control-treeview, root-node-height); + min-height: getCssVar(control-treeview, node, height); + max-height: getCssVar(control-treeview, root-node-max-height); + + >.el-tree-node__children { + height: calc(100% - var(--el-tree-node-content-height)); + + // element行内样式写死,此处使用important + overflow-y: auto !important; + } + + &.is-transition { + transition: height 0.3s ease-in-out; + } + } + + >.el-tree-node.is-expanded { + flex-grow: getCssVar(control-treeview, root-node-grow); + flex-shrink: 0; + will-change: height; + + &+.el-tree-node.is-expanded { + .#{bem(control-treeview, split)} { + display: block; + } + } + } + .#{bem(control-treeview, split)} { + position: absolute; + left: 0; + display: none; + width: 100%; + height: getCssVar(spacing, extra-tight); + margin-bottom: var(--el-tree-node-content-height); + cursor: s-resize; + border-top: 1px solid getCssVar(color, border); + } + } +} \ No newline at end of file diff --git a/src/control/tree/tree.tsx b/src/control/tree/tree.tsx index 3619c886ad61888ce5377a077014c8d8a6d64fe1..47c7f03d4968b5e30e4732fa5759efa5893c6d61 100644 --- a/src/control/tree/tree.tsx +++ b/src/control/tree/tree.tsx @@ -226,7 +226,10 @@ export const TreeControl = defineComponent({ } }; - const { updateUI, triggerNodeExpand } = useElTreeUtil(treeRef, c); + const { updateUI, triggerNodeExpand, onSplitMousedown } = useElTreeUtil( + treeRef, + c, + ); /** * 创建新的节点对象,隔离组件数据和controller数据 @@ -785,6 +788,20 @@ export const TreeControl = defineComponent({ ); }; + const renderSplitLine = (node: IData) => { + const { treeStyle } = c.model; + if (treeStyle !== 'USER' || node.level !== 1) { + return null; + } + return ( +