diff --git a/packages/layout-design/src/design-index-view/components/design-left-menu/design-left-menu.scss b/packages/layout-design/src/design-index-view/components/design-left-menu/design-left-menu.scss index febdc1115e602c1d3ebc51661cc2388359986a94..94776680bf0ee2350340df9ec344cd5bea433403 100644 --- a/packages/layout-design/src/design-index-view/components/design-left-menu/design-left-menu.scss +++ b/packages/layout-design/src/design-index-view/components/design-left-menu/design-left-menu.scss @@ -55,4 +55,4 @@ cursor: col-resize; background-color: getCssVar(color, border); } -} \ No newline at end of file +} diff --git a/packages/layout-design/src/design-index-view/components/design-left-menu/design-left-menu.tsx b/packages/layout-design/src/design-index-view/components/design-left-menu/design-left-menu.tsx index 50b59864f1d36a95556a25558e6c6c00070a40fd..77830c0e1ed417cd78e45af439f427bf511c81f3 100644 --- a/packages/layout-design/src/design-index-view/components/design-left-menu/design-left-menu.tsx +++ b/packages/layout-design/src/design-index-view/components/design-left-menu/design-left-menu.tsx @@ -41,7 +41,7 @@ export default defineComponent({ const totalWidth = document.getElementById('app')!.offsetWidth; let animationFrameId: number; // AnimationFrame 确保界面更新与浏览器的重绘周期对齐,从而提高性能和流畅度 - const moveAt = (pageX: number, pageY: number): void => { + const moveAt = (pageX: number): void => { if (animationFrameId) { cancelAnimationFrame(animationFrameId); } @@ -51,15 +51,17 @@ export default defineComponent({ diffX >= (c.leftContainer!.model.width || 48) && diffX <= totalWidth / 2 ) { - c.leftContainer!.state.layout.width = `${(diffX / totalWidth) * 100}%`; + c.leftContainer!.state.layout.width = `${ + (diffX / totalWidth) * 100 + }%`; } }); }; - - moveAt(evt.pageX, evt.pageY); + + moveAt(evt.pageX); const onMouseMove = (event: MouseEvent): void => { - moveAt(event.pageX, event.pageY); + moveAt(event.pageX); }; document.addEventListener('mousemove', onMouseMove); diff --git a/packages/layout-design/src/design-index-view/components/design-right-menu/design-right-menu.controller.ts b/packages/layout-design/src/design-index-view/components/design-right-menu/design-right-menu.controller.ts index 5c131ab92d7c706086ecaf51c326b37db45be95a..e3c7cb28aaba7cece21a760674b826199883646f 100644 --- a/packages/layout-design/src/design-index-view/components/design-right-menu/design-right-menu.controller.ts +++ b/packages/layout-design/src/design-index-view/components/design-right-menu/design-right-menu.controller.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { AppFuncCommand, getControl, diff --git a/packages/layout-design/src/design-index-view/components/design-right-menu/design-right-menu.scss b/packages/layout-design/src/design-index-view/components/design-right-menu/design-right-menu.scss index 0029408b8bb36cad937a1b5cd082ad5d236f7472..4152a2b300eedb2d47c7e937cfd3211c578683e9 100644 --- a/packages/layout-design/src/design-index-view/components/design-right-menu/design-right-menu.scss +++ b/packages/layout-design/src/design-index-view/components/design-right-menu/design-right-menu.scss @@ -1,7 +1,7 @@ @include b(design-right-menu) { position: fixed; right: getCssVar(spacing, tight); - bottom: getCssVar(spacing, tight); + bottom: 24px; z-index: 9999; @include e(button) { diff --git a/packages/layout-design/src/design-index-view/components/design-right-menu/design-right-menu.tsx b/packages/layout-design/src/design-index-view/components/design-right-menu/design-right-menu.tsx index 10ad0943e2f55f34b26a7322af94dc221aafa5d8..3c317b4b4daa09f9e2f6a6ebc3b0413fa6e32799 100644 --- a/packages/layout-design/src/design-index-view/components/design-right-menu/design-right-menu.tsx +++ b/packages/layout-design/src/design-index-view/components/design-right-menu/design-right-menu.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { PropType, defineComponent, ref } from 'vue'; import { useNamespace } from '@ibiz-template/vue3-util'; import { IPanelRawItem } from '@ibiz/model-core'; diff --git a/packages/layout-design/src/design-index-view/components/design-tree/design-tree.controller.ts b/packages/layout-design/src/design-index-view/components/design-tree/design-tree.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..b1e8629577781c2744bf0e128eca9eaf0212636a --- /dev/null +++ b/packages/layout-design/src/design-index-view/components/design-tree/design-tree.controller.ts @@ -0,0 +1,166 @@ +import { + ITreeEvent, + ITreeNodeData, + TreeController, + UIActionUtil, + ITreeState, +} from '@ibiz-template/runtime'; +import { IDETree } from '@ibiz/model-core'; + +/** + * 设计树控制器 + * + * @export + * @class DesignTreeController + * @extends {TreeController} + * @template T + * @template S + * @template E + */ +export class DesignTreeController< + T extends IDETree = IDETree, + S extends ITreeState = ITreeState, + E extends ITreeEvent = ITreeEvent, +> extends TreeController { + /** + * 执行界面行为 + * - 合并currentSrfNav到上下文文中 + * @param {string} uiActionId + * @param {ITreeNodeData} nodeData + * @param {MouseEvent} event + * @param {string} appId + * @return {*} {Promise} + * @memberof DesignTreeController + */ + async doUIAction( + uiActionId: string, + nodeData: ITreeNodeData, + event: MouseEvent, + appId: string, + ): Promise { + const eventArgs = this.getEventArgs(); + // 合并currentSrfNav + eventArgs.context.currentSrfNav = nodeData._id; + const nodeParams = this.parseTreeNodeData(nodeData); + const result = await UIActionUtil.exec( + uiActionId!, + { + ...eventArgs, + ...nodeParams, + event, + }, + appId, + ); + if (result.closeView) { + this.view.closeView(); + } else if (result.refresh) { + switch (result.refreshMode) { + // 刷新当前节点的子 + case 1: + this.refreshNodeChildren(nodeData); + break; + // 刷新当前节点的父节点的子 + case 2: + this.refreshNodeChildren(nodeData, true); + break; + // 刷新所有节点数据 + case 3: + this.refresh(); + break; + default: + } + } + } + + /** + * 重写树节点点击事件 + * - 先设置选中 + * @param {ITreeNodeData} nodeData + * @param {MouseEvent} event + * @return {*} {Promise} + * @memberof DesignTreeController + */ + async onTreeNodeClick( + nodeData: ITreeNodeData, + event: MouseEvent, + ): Promise { + // 单选时,单击才会触发选中逻辑,禁止选择的时候不触发 + if (this.state.singleSelect && !nodeData._disableSelect) { + // 选中相关处理 + const { selectedData } = this.state; + // 选中里没有则添加,有则删除 + const filterArr = selectedData.filter(item => item._id !== nodeData._id); + if (filterArr.length === selectedData.length) { + this.setSelection( + this.state.singleSelect + ? [nodeData] + : selectedData.concat([nodeData]), + ); + } else { + this.setSelection(filterArr); + } + } + // 节点有配置常用操作的上下文菜单时,触发界面行为,后续逻辑都不走 + const clickActionItem = + this.contextMenuInfos[nodeData._nodeId]?.clickTBUIActionItem; + if (clickActionItem) { + return this.doUIAction( + clickActionItem.uiactionId!, + nodeData, + event, + clickActionItem.appId, + ); + } + + // 导航的时候,没有导航视图的时候,节点后续点击逻辑都不走,也不选中 + if (this.state.navigational) { + const nodeModel = this.getNodeModel(nodeData._nodeId); + if (!nodeModel?.navAppViewId) { + return; + } + } + + // 激活事件 + if (this.state.mdctrlActiveMode === 1) { + await this.setActive(nodeData); + } + } + + /** + * 根据srfnav计算需要展开的节点标识 + * + * @param {string} srfnav + * @return {*} {string[]} + * @memberof DesignTreeController + */ + calcExpandKeys(srfnav: string): string[] { + const expandedKeys: string[] = []; + srfnav.split(':').forEach((item, index) => { + if (index === 0) { + expandedKeys.push(item); + } else { + expandedKeys.push(`${expandedKeys[index - 1]}:${item}`); + } + }); + expandedKeys.pop(); + return expandedKeys; + } + + /** + * 默认选中 + * + * @param {string} srfnav 导航参数 + * @return {*} {Promise} + * @memberof DesignTreeController + */ + async onDefaultSelect(srfnav?: string): Promise { + if (srfnav) { + const expandKeys = this.calcExpandKeys(srfnav); + await this.expandNodeByKey(expandKeys); + const selectItem = this.state.items.find(item => item._id === srfnav); + if (selectItem) { + this.setSelection([selectItem]); + } + } + } +} diff --git a/packages/layout-design/src/design-index-view/components/design-tree/design-tree.provider.ts b/packages/layout-design/src/design-index-view/components/design-tree/design-tree.provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..a7b4e721f4b3a63bcb15c0dcfd4f3a8375e60c3a --- /dev/null +++ b/packages/layout-design/src/design-index-view/components/design-tree/design-tree.provider.ts @@ -0,0 +1,12 @@ +import { IControlProvider } from '@ibiz-template/runtime'; + +/** + * 设计树 + * + * @export + * @class DesignTreeProvider + * @implements {IControlProvider} + */ +export class DesignTreeProvider implements IControlProvider { + component: string = 'IBizDesignTreeControl'; +} diff --git a/packages/layout-design/src/design-index-view/components/design-tree/design-tree.scss b/packages/layout-design/src/design-index-view/components/design-tree/design-tree.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/packages/layout-design/src/design-index-view/components/design-tree/design-tree.tsx b/packages/layout-design/src/design-index-view/components/design-tree/design-tree.tsx new file mode 100644 index 0000000000000000000000000000000000000000..834a11e35bbd36a6136b7ced380afc83eb993545 --- /dev/null +++ b/packages/layout-design/src/design-index-view/components/design-tree/design-tree.tsx @@ -0,0 +1,771 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { + useControlController, + useNamespace, + route2routePath, +} from '@ibiz-template/vue3-util'; +import { + computed, + defineComponent, + nextTick, + onMounted, + onUnmounted, + PropType, + reactive, + ref, + resolveComponent, + VNode, + watch, +} from 'vue'; +import { MenuItem } from '@imengyu/vue3-context-menu'; +import { createUUID } from 'qx-util'; +import { debounce } from 'lodash-es'; +import { + IDETBGroupItem, + IDETBRawItem, + IDETBUIActionItem, + IDEToolbarItem, + IDETree, + IDETreeNode, +} from '@ibiz/model-core'; +import { + IButtonContainerState, + IButtonState, + IControlProvider, + ITreeNodeData, + getControlPanel, +} from '@ibiz-template/runtime'; +import { RuntimeError } from '@ibiz-template/core'; +import { ElTree } from 'element-plus'; +import { + AllowDropType, + NodeDropType, +} from 'element-plus/es/components/tree/src/tree.type'; +import { isNil } from 'ramda'; +import { useRoute } from 'vue-router'; +import { DesignTreeController } from './design-tree.controller'; +import { + findNodeData, + formatNodeDropType, + useElTreeUtil, +} from './el-tree-util'; +import './design-tree.scss'; + +export const DesignTreeControl = defineComponent({ + name: 'IBizDesignTreeControl', + props: { + modelData: { type: Object as PropType, required: true }, + context: { type: Object as PropType, required: true }, + params: { type: Object as PropType, default: () => ({}) }, + provider: { type: Object as PropType }, + mdctrlActiveMode: { type: Number, default: undefined }, + singleSelect: { type: Boolean, default: undefined }, + navigational: { type: Boolean, default: undefined }, + defaultExpandedKeys: { type: Array as PropType }, + loadDefault: { type: Boolean, default: true }, + checkStrictly: { type: Boolean, default: true }, + }, + setup() { + const c = useControlController( + (...args) => new DesignTreeController(...args), + ); + const ns = useNamespace(`control-${c.model.controlType!.toLowerCase()}`); + const ns2 = useNamespace('design-tree'); + + // 初始化默认选中 + const route = useRoute(); + c.evt.on('onLoadSuccess', () => { + const routePath = route2routePath(route); + const { srfnav } = routePath.pathNodes[0]; + c.onDefaultSelect(srfnav); + }); + + const cascadeSelect = ref(false); + const counterData = reactive({}); + const fn = (counter: IData): void => { + Object.assign(counterData, counter); + }; + c.evt.on('onCreated', () => { + if (c.counter) { + c.counter.onChange(fn, true); + } + if (c.controlParams.cascadeselect) { + cascadeSelect.value = true; + } + }); + + onUnmounted(() => { + c.counter?.offChange(fn); + }); + + const treeRef = ref | null>(null); + const treeviewRef = ref(null); + + const treeRefreshKey = ref(''); + + // 节点名称编辑相关 + const treeNodeTextInputRef = ref(null); + const editingNodeKey = ref(null); + const editingNodeText = ref(null); + + watch( + () => treeNodeTextInputRef.value, + newVal => { + if (newVal) { + newVal.$el.getElementsByTagName('input')[0].focus(); + } + }, + ); + + /** + * 编辑当前节点的文本 + * @author lxm + * @date 2023-12-15 05:46:02 + * @return {*} {void} + */ + const editCurrentNodeText = (): void => { + const currentkey = treeRef.value?.getCurrentKey(); + if (!currentkey || currentkey === editingNodeKey.value) { + return; + } + const nodeData = findNodeData(currentkey, c)!; + const model = c.getNodeModel(nodeData._nodeId); + if (model?.allowEditText) { + editingNodeKey.value = currentkey; + } + }; + + /** + * 处理节点文本编辑失效事件 + * @author lxm + * @date 2023-12-15 05:37:03 + */ + const onNodeTextEditBlur = async () => { + if (editingNodeKey.value) { + if (editingNodeText.value) { + const nodeData = findNodeData(editingNodeKey.value, c)!; + await c.modifyNodeText(nodeData, editingNodeText.value); + editingNodeKey.value = null; + editingNodeText.value = null; + } else { + // 取消编辑 + editingNodeKey.value = null; + } + } + }; + + const { updateUI, triggerNodeExpand } = useElTreeUtil(treeRef, c); + + /** + * 创建新的节点对象,隔离组件数据和controller数据 + * @author lxm + * @date 2023-08-30 09:35:25 + * @param {ITreeNodeData[]} nodes + * @return {*} {IData[]} + */ + const toElNodes = (nodes: ITreeNodeData[]): IData[] => { + return nodes.map(node => ({ + _id: node._id, + _uuid: node._uuid, + _leaf: node._leaf, + _text: node._text, + _disableSelect: node._disableSelect, + })); + }; + + // el-treeUI更新子节点 + c.evt.on('onAfterRefreshParent', event => { + if (treeRef.value) { + const { parentNode, children } = event; + const elNodes = toElNodes(children); + treeRef.value!.updateKeyChildren(parentNode._id, elNodes); + updateUI(); + } + }); + + c.evt.on('onAfterNodeDrop', event => { + if (event.isChangedParent) { + // 变更父节点的时候强刷 + treeRefreshKey.value = createUUID(); + } + }); + + /** 树展示数据 */ + const treeData = computed(() => { + if (!c.state.isLoaded) { + return []; + } + return c.model.rootVisible + ? c.state.rootNodes + : c.state.rootNodes.reduce((result, nodeData) => { + if (nodeData._children) { + return result.concat(nodeData._children); + } + return result; + }, []); + }); + + // 根节点数据变更时重绘tree + watch(treeData, (newVal, oldVal) => { + if (newVal !== oldVal) { + treeRefreshKey.value = createUUID(); + } + }); + + /** + * 触发节点加载数据 + * @author lxm + * @date 2023-05-29 09:16:07 + * @param {IData} item + * @param {(nodes: IData[]) => void} callback + */ + const loadData = async ( + item: IData, + callback: (nodes: IData[]) => void, + ) => { + let nodes: ITreeNodeData[]; + if (item.level === 0) { + nodes = treeData.value; + ibiz.log.debug('初始加载'); + } else { + const nodeData = findNodeData(item.data._uuid, c)!; + if (nodeData._children) { + ibiz.log.debug('节点展开加载-本地', nodeData); + nodes = nodeData._children; + } else { + ibiz.log.debug('节点展开加载-远程', nodeData); + nodes = await c.loadNodes(nodeData); + } + } + ibiz.log.debug('给树返回值', nodes); + + callback(toElNodes(nodes)); + updateUI(); + }; + + // 值变更优化,加载成功后的值变更需要等渲染完成之后执行,其他情况不用 + let selectionWait = false; + c.evt.on('onLoadSuccess', () => { + selectionWait = true; + setTimeout(() => { + selectionWait = false; + }, 200); + }); + + // 选中数据回显 + c.evt.on('onSelectionChange', async () => { + if (selectionWait) { + await nextTick(); + } + if (c.state.singleSelect) { + treeRef.value!.setCurrentKey(c.state.selectedData[0]?._id || undefined); + } else { + // el-tree,会把没选中的反选,且不触发check事件 + treeRef.value!.setCheckedKeys( + c.state.selectedData.map(item => item._id), + ); + } + }); + + /** + * 多选时选中节点变更 + */ + const onCheck = ( + nodeData: ITreeNodeData, + opts: { checkedNodes: ITreeNodeData[] }, + ) => { + const { checkedNodes } = opts; + c.setSelection(checkedNodes); + }; + + let forbidClick: boolean = false; + + const readonly = computed(() => { + return !!( + c.context.srfreadonly === true || c.context.srfreadonly === 'true' + ); + }); + /** + * 节点单击事件 + */ + const onNodeClick = (nodeData: ITreeNodeData, evt: MouseEvent) => { + evt.stopPropagation(); + if (nodeData._disableSelect) return; + if (forbidClick) { + return; + } + + // 已经是当前节点,则进入编辑模式 + if (treeRef.value?.getCurrentKey() === nodeData._id && !readonly.value) { + editCurrentNodeText(); + } + + // 多选的时候设置节点的当前节点 + if (!c.state.singleSelect) { + treeRef.value?.setCurrentKey(nodeData._id); + } + + // 导航树节点不配置导航视图的时候,只切换展开状态 + if (c.state.navigational) { + const nodeModel = c.getNodeModel(nodeData._nodeId); + if (!nodeModel?.navAppViewId) { + const expanded = triggerNodeExpand(nodeData._id); + c.onExpandChange(nodeData, expanded!); + } + } + + c.onTreeNodeClick(nodeData, evt); + forbidClick = true; + setTimeout(() => { + forbidClick = false; + }, 200); + }; + + /** + * 节点双击事件 + */ + const onNodeDbClick = (nodeData: ITreeNodeData, evt: MouseEvent) => { + evt.stopPropagation(); + if (nodeData._disableSelect) return; + c.onDbTreeNodeClick(nodeData); + }; + + // *上下文菜单相关 / + + let ContextMenu: IData; + c.evt.on('onMounted', () => { + // 有上下文菜单时加载组件 + if (Object.values(c.contextMenus).length > 0) { + const importMenu = () => import('@imengyu/vue3-context-menu'); + importMenu().then(value => { + ContextMenu = value.default; + if (ContextMenu.default && !ContextMenu.showContextMenu) { + ContextMenu = ContextMenu.default; + } + }); + } + }); + + const iBizRawItem = resolveComponent('IBizRawItem'); + const iBizIcon = resolveComponent('IBizIcon'); + + /** + * 计算上下文菜单组件配置项集合 + */ + const calcContextMenuItems = ( + toolbarItems: IDEToolbarItem[], + nodeData: ITreeNodeData, + evt: MouseEvent, + menuState: IButtonContainerState, + ): MenuItem[] => { + const result: MenuItem[] = []; + toolbarItems.forEach(item => { + if (item.itemType === 'SEPERATOR') { + result.push({ + divided: 'self', + }); + return; + } + + const buttonState = menuState[item.id!] as IButtonState; + if (buttonState && !buttonState.visible) { + return; + } + + // 除分隔符之外的公共部分 + const menuItem: MenuItem = {}; + if (item.showCaption && item.caption) { + menuItem.label = item.caption; + } + if (item.sysImage && item.showIcon) { + menuItem.icon = ; + } + + // 界面行为项 + if (item.itemType === 'DEUIACTION') { + menuItem.disabled = buttonState.disabled; + menuItem.clickClose = true; + const { uiactionId } = item as IDETBUIActionItem; + if (uiactionId) { + menuItem.onClick = () => { + c.doUIAction(uiactionId, nodeData, evt, item.appId); + }; + } + } else if (item.itemType === 'RAWITEM') { + const { rawItem } = item as IDETBRawItem; + if (rawItem) { + menuItem.label = ( + + ); + } + } else if (item.itemType === 'ITEMS') { + // 分组项绘制子菜单 + if ((item as IDETBGroupItem).detoolbarItems?.length) { + menuItem.children = calcContextMenuItems( + (item as IDETBGroupItem).detoolbarItems!, + nodeData, + evt, + menuState, + ); + } + } + result.push(menuItem); + }); + + return result; + }; + + /** + * 节点右键菜单点击事件 + */ + const onNodeContextmenu = async ( + nodeData: ITreeNodeData, + evt: MouseEvent, + ) => { + // 阻止原生浏览器右键菜单打开 + evt.preventDefault(); + evt.stopPropagation(); + const nodeModel = c.getNodeModel(nodeData._nodeId); + if (!nodeModel?.decontextMenu) { + return; + } + const contextMenuC = c.contextMenus[nodeModel.decontextMenu.id!]; + + if (!contextMenuC.model.detoolbarItems) { + return; + } + + // 更新菜单的权限状态 + await contextMenuC.calcButtonState( + nodeData._deData || (nodeData.srfkey ? nodeData : undefined), + nodeModel.appDataEntityId, + ); + const menuState = contextMenuC.state.buttonsState; + + const menus: MenuItem[] = calcContextMenuItems( + contextMenuC.model.detoolbarItems, + nodeData, + evt, + menuState, + ); + if (!menus.length) { + return; + } + + ContextMenu.showContextMenu({ + x: evt.x, + y: evt.y, + customClass: ns.b('context-menu'), + items: menus, + }); + }; + + /** + * 绘制上下文菜单触发图标 + * @param nodeModel + * @param nodeData + * @returns + */ + const renderContextMenu = ( + nodeModel: IDETreeNode, + nodeData: ITreeNodeData, + ) => { + if (!nodeModel?.decontextMenu?.detoolbarItems?.length) { + return; + } + + // 只有一个界面行为项时,且是点击触发的界面行为时,不绘制。。。 + const menuInfo = c.contextMenuInfos[nodeModel.id!]; + if (menuInfo.clickTBUIActionItem && menuInfo.onlyOneActionItem) { + return null; + } + + return ( + + c.doUIAction(detail.uiactionId!, nodeData, e, detail.appId) + } + > + ); + }; + + const updateNodeExpand = (data: IData, expanded: boolean) => { + const nodeData = findNodeData(data._uuid, c); + if (!nodeData) { + throw new RuntimeError( + ibiz.i18n.t('control.common.noFoundNode', { id: data._uuid }), + ); + } + c.onExpandChange(nodeData, expanded); + }; + + const debounceSearch = debounce(() => { + c.load(); + }, 500); + + const onInput = (value: string): void => { + c.state.query = value; + debounceSearch(); + }; + + // 拖拽相关 + const allowDrop = ( + draggingNode: IData, + dropNode: IData, + type: AllowDropType, + ) => { + const draggingNodeData = findNodeData(draggingNode.data._uuid, c)!; + const dropNodeData = findNodeData(dropNode.data._uuid, c)!; + const result = c.calcAllowDrop(draggingNodeData, dropNodeData, type); + return result; + }; + + const allowDrag = (draggingNode: IData) => { + const nodeData = findNodeData(draggingNode.data._uuid, c)!; + return c.calcAllowDrag(nodeData); + }; + + /** + * 处理拖入完成事件 + * @author lxm + * @date 2023-12-15 05:37:10 + * @param {IData} draggingNode + * @param {IData} dropNode + * @param {NodeDropType} dropType + */ + const handleDrop = ( + draggingNode: IData, + dropNode: IData, + dropType: NodeDropType, + ) => { + const type = formatNodeDropType(dropType); + const draggingNodeData = findNodeData(draggingNode.data._uuid, c)!; + const dropNodeData = findNodeData(dropNode.data._uuid, c)!; + c.onNodeDrop(draggingNodeData, dropNodeData, type); + }; + + /** + * 处理按键事件 + * @author lxm + * @date 2023-12-15 05:39:00 + * @param {KeyboardEvent} e + */ + const keydownHandle = (e: KeyboardEvent) => { + if (e.code === 'F2' || e.code === 'Enter') { + editCurrentNodeText(); + } + }; + + onMounted(() => { + treeviewRef.value?.$el.addEventListener('keydown', keydownHandle); + }); + + onUnmounted(() => { + treeviewRef.value?.$el.removeEventListener('keydown', keydownHandle); + }); + + const renderCounter = (nodeModel: IDETreeNode) => { + if (nodeModel.counterId) { + const value = counterData[nodeModel.counterId]; + if (isNil(value)) { + return null; + } + if (nodeModel.counterMode === 1 && value === 0) { + return null; + } + return ; + } + }; + + return { + c, + ns, + ns2, + treeRef, + treeviewRef, + treeNodeTextInputRef, + treeData, + treeRefreshKey, + editingNodeKey, + editingNodeText, + cascadeSelect, + findNodeData, + onCheck, + onNodeClick, + onNodeDbClick, + onNodeContextmenu, + loadData, + renderContextMenu, + renderCounter, + updateNodeExpand, + onInput, + allowDrop, + allowDrag, + handleDrop, + onNodeTextEditBlur, + }; + }, + render() { + const slots: IData = { + searchbar: () => { + if (!this.c.enableQuickSearch) { + return null; + } + return ( + + {{ + prefix: (): VNode => { + return ( + + ); + }, + }} + + ); + }, + }; + const key = this.c.controlPanel ? 'tree' : 'default'; + slots[key] = () => { + if (this.c.state.isLoaded && this.treeRefreshKey) { + return ( + { + this.updateNodeExpand(data, true); + }} + onNodeCollapse={(data: IData) => { + this.updateNodeExpand(data, false); + }} + draggable={true} + allow-drop={this.allowDrop} + allow-drag={this.allowDrag} + onNodeDrop={this.handleDrop} + > + {{ + default: ({ data }: { node: IData; data: IData }) => { + const nodeData = this.findNodeData(data._uuid, this.c)!; + if (!nodeData) { + return null; + } + const nodeModel = this.c.getNodeModel(nodeData._nodeId)!; + + // 绘制编辑项 + if (this.editingNodeKey === nodeData._id) { + return ( +
+ { + this.onNodeTextEditBlur(); + }} + onKeydown={(e: KeyboardEvent) => { + e.stopPropagation(); + if (e.code === 'Enter') { + this.onNodeTextEditBlur(); + } + }} + > +
+ ); + } + + const layoutPanel = getControlPanel(nodeModel); + + let content; + if (layoutPanel) { + content = ( + + ); + } else { + content = [ + nodeData._icon ? ( + + ) : null, + nodeData._textHtml ? ( + + ) : ( + + {nodeData._text} + + ), + ]; + } + + return ( +
this.onNodeDbClick(nodeData, evt)} + onClick={evt => this.onNodeClick(nodeData, evt)} + onContextmenu={evt => this.onNodeContextmenu(nodeData, evt)} + class={[ + this.ns.b('node'), + nodeData._disableSelect + ? this.ns.bm('node', 'disabled') + : '', + nodeModel.sysCss?.cssName, + ]} + > + {content} + {this.renderCounter(nodeModel)} + {this.renderContextMenu(nodeModel, nodeData)} +
+ ); + }, + }} +
+ ); + } + }; + + return ( + + {slots} + + ); + }, +}); diff --git a/packages/layout-design/src/design-index-view/components/design-tree/el-tree-util.ts b/packages/layout-design/src/design-index-view/components/design-tree/el-tree-util.ts new file mode 100644 index 0000000000000000000000000000000000000000..2e837231ccc9b170bcda84f007e74108a2473ade --- /dev/null +++ b/packages/layout-design/src/design-index-view/components/design-tree/el-tree-util.ts @@ -0,0 +1,124 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { RuntimeError } from '@ibiz-template/core'; +import { ITreeController, ITreeNodeData } from '@ibiz-template/runtime'; +import { ElTree } from 'element-plus'; +import { NodeDropType } from 'element-plus/es/components/tree/src/tree.type'; +import { debounce } from 'lodash-es'; +import { Ref } from 'vue'; + +/** + * 根据id查找树节点数据对象 + * @author lxm + * @date 2023-09-28 03:45:35 + * @export + * @param {string} key (数据的_uuid 或者 _id) + * @param {ITreeController} c + * @return {*} {(ITreeNodeData | undefined)} + */ +export function findNodeData( + key: string, + c: ITreeController, +): ITreeNodeData | undefined { + const find = c.state.items.find(item => item._id === key); + if (find) { + return find; + } + return c.state.items.find(item => item._uuid === key); +} + +export function useElTreeUtil( + treeRef: Ref | null>, + c: ITreeController, +) { + const getTreeInstance = () => { + const elTree = treeRef.value; + if (!elTree) { + throw new RuntimeError(ibiz.i18n.t('control.tree.noFoundInstance')); + } + return elTree; + }; + + const _updateUI = () => { + const elTree = treeRef.value; + if (!elTree) { + setTimeout(() => { + _updateUI(); + }, 200); + return; + } + // 设置节点展开 + Object.values(elTree.store.nodesMap).forEach(node => { + const shouldExpanded = c.state.expandedKeys.includes(node.data._id); + if (shouldExpanded !== node.expanded) { + if (shouldExpanded) { + node.expand(); + } else { + node.collapse(); + } + } + }); + // 设置选中状态。 + if (c.state.singleSelect) { + treeRef.value!.setCurrentKey(c.state.selectedData[0]?._id || undefined); + } else { + // el-tree,会把没选中的反选,且不触发check事件 + elTree.setCheckedKeys(c.state.selectedData.map(item => item._id)); + } + }; + + /** + * 更新树的UI状态,如选中状态和展开状态。 + * 在树组件的loadData和调用updateKeyChildren之后去更新 + * 加了防抖,所以尽可能多的调用,确保状态 todo + * @author lxm + * @date 2023-09-28 03:36:44 + */ + const updateUI = debounce(_updateUI, 500) as typeof _updateUI; + + /** + * 切换节点的展开 + * @author lxm + * @date 2023-11-08 03:57:27 + * @param {string} id + */ + const triggerNodeExpand = (id: string): boolean | undefined => { + const elTree = getTreeInstance(); + const target = elTree.store.nodesMap[id]; + if (target) { + if (target.expanded) { + target.collapse(); + return false; + } + target.expand(); + return true; + } + }; + + return { getTreeInstance, updateUI, triggerNodeExpand }; +} + +/** + * 格式化dropType + * @author lxm + * @date 2023-12-14 02:13:10 + * @export + * @param {NodeDropType} dropType + * @return {*} {('inner' | 'prev' | 'next')} + */ +export function formatNodeDropType( + dropType: NodeDropType, +): 'inner' | 'prev' | 'next' { + switch (dropType) { + case 'inner': + return 'inner'; + case 'before': + return 'prev'; + case 'after': + return 'next'; + default: + throw new RuntimeError( + ibiz.i18n.t('control.tree.noSupported', { dropType }), + ); + } +} diff --git a/packages/layout-design/src/design-index-view/components/index.ts b/packages/layout-design/src/design-index-view/components/index.ts index 23218c1a287c9d3287cb9c86d147177dc0bb076c..1d1bd2be46d72ca5b41fdc8918eaa97157c4dc61 100644 --- a/packages/layout-design/src/design-index-view/components/index.ts +++ b/packages/layout-design/src/design-index-view/components/index.ts @@ -1,11 +1,16 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { App } from 'vue'; -import { registerPanelItemProvider } from '@ibiz-template/runtime'; +import { + registerControlProvider, + registerPanelItemProvider, +} from '@ibiz-template/runtime'; import IBizDesignLeftMenu from './design-left-menu/design-left-menu'; import { DesignLeftMenuProvider } from './design-left-menu/design-left-menu.provider'; import IBizDesignRightMenu from './design-right-menu/design-right-menu'; import { DesignRightMenuProvider } from './design-right-menu/design-right-menu.provider'; +import { DesignTreeControl } from './design-tree/design-tree'; +import { DesignTreeProvider } from './design-tree/design-tree.provider'; export default { install(app: App) { @@ -20,5 +25,10 @@ export default { 'RAWITEM_RIGHT_SIDE_MENU', () => new DesignRightMenuProvider(), ); + app.component('IBizDesignTreeControl', DesignTreeControl); + registerControlProvider( + 'TREE_RENDER_DESIGN_TREE', + () => new DesignTreeProvider(), + ); }, };