diff --git a/CHANGELOG.md b/CHANGELOG.md index 625c0c36edb33d7fd722b8e4e02f6449c4aa40cc..84a54dec01d616a24f34b19ccf0e50f48da4b60f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ 并且此项目遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/). ## [Unreleased] +### Added + +- 菜单部件支持分割项扩展模式 ### Fixed diff --git a/src/common/control-navigation/control-navigation.scss b/src/common/control-navigation/control-navigation.scss new file mode 100644 index 0000000000000000000000000000000000000000..843c1447bb98749e213e71b0551130e2dbc4c805 --- /dev/null +++ b/src/common/control-navigation/control-navigation.scss @@ -0,0 +1,15 @@ +@include b(control-navigation) { + width: 100%; + height: 100%; + @include e(nav-control) { + position: relative; + @include m(icon) { + top: 0; + right: 0; + z-index: 1; + cursor: pointer; + position: absolute; + color: getCssVar(color, primary); + } + } +} diff --git a/src/common/control-navigation/control-navigation.tsx b/src/common/control-navigation/control-navigation.tsx new file mode 100644 index 0000000000000000000000000000000000000000..85dd65b7ab87dc9cd2db5888e82ae603b725160e --- /dev/null +++ b/src/common/control-navigation/control-navigation.tsx @@ -0,0 +1,122 @@ +/* eslint-disable no-nested-ternary */ +import { EventBase, MDControlController } from '@ibiz-template/runtime'; +import { useNamespace } from '@ibiz-template/vue3-util'; +import { defineComponent, h, PropType, resolveComponent } from 'vue'; +import { useControlNav } from './control-navigation.util'; +import './control-navigation.scss'; + +/** + * 部件内容导航组件 + */ +export const IBizControlNavigation = defineComponent({ + name: 'IBizControlNavigation', + props: { + controller: { + type: Object as PropType, + required: true, + }, + }, + setup(props, { slots }) { + const ns = useNamespace('control-navigation'); + + const { + style, + enableNav, + splitMode, + splitValue, + showNavView, + showNavIcon, + navViewMsg, + } = useControlNav(props.controller); + + const onViewCreated = (event: EventBase): void => { + ibiz.log.debug(props.controller.name, 'onViewCreated', event); + }; + + const onShowViewChange = (event: MouseEvent) => { + event.stopPropagation(); + event.preventDefault(); + showNavView.value = !showNavView.value; + }; + + const renderNavControl = () => { + return ( +
+ {slots.default?.()} + {showNavIcon.value ? ( + + {!showNavView.value ? ( + + ) : ( + + )} + + ) : null} +
+ ); + }; + + const renderNavView = () => { + if (navViewMsg.value) { + return h(resolveComponent('IBizViewShell'), { + ...navViewMsg.value, + class: ns.e('nav-view'), + onCreated: onViewCreated, + }); + } + }; + + return { + ns, + style, + enableNav, + splitMode, + splitValue, + navViewMsg, + showNavView, + renderNavView, + renderNavControl, + }; + }, + render() { + return ( +
+ {this.enableNav ? ( + this.showNavView ? ( + + {{ + left: () => this.renderNavControl(), + right: () => this.renderNavView(), + top: () => this.renderNavControl(), + bottom: () => this.renderNavView(), + }} + + ) : ( + this.renderNavControl() + ) + ) : ( + this.$slots.default?.() + )} +
+ ); + }, +}); diff --git a/src/common/control-navigation/control-navigation.util.ts b/src/common/control-navigation/control-navigation.util.ts new file mode 100644 index 0000000000000000000000000000000000000000..0546c38be87f3022ca8718d88ce661d551ad8546 --- /dev/null +++ b/src/common/control-navigation/control-navigation.util.ts @@ -0,0 +1,326 @@ +import { + IDER1N, + IDETree, + IDETreeNode, + INavigatable, + IControlNavigatable, +} from '@ibiz/model-core'; +import { + INavViewMsg, + calcNavParams, + IMDControlEvent, + MDControlController, + calcDeCodeNameById, +} from '@ibiz-template/runtime'; +import { CSSProperties, Ref, ref } from 'vue'; +import { RuntimeError } from '@ibiz-template/core'; + +/** + * 获取导航视图样式 + * + * @param {IControlNavigatable} control + * @return {*} {CSSProperties} + */ +function getNavViewStyle(control: IControlNavigatable): CSSProperties { + const { + navViewWidth, + navViewHeight, + navViewMinWidth, + navViewMaxWidth, + navViewMinHeight, + navViewMaxHeight, + } = control; + return { + width: navViewWidth ? `${navViewWidth}px` : undefined, + height: navViewHeight ? `${navViewHeight}px` : undefined, + minWidth: navViewMinWidth ? `${navViewMinWidth}px` : undefined, + maxWidth: navViewMaxWidth ? `${navViewMaxWidth}px` : undefined, + minHeight: navViewMinHeight ? `${navViewMinHeight}px` : undefined, + maxHeight: navViewMaxHeight ? `${navViewMaxHeight}px` : undefined, + }; +} + +/** + * 解析导航参数 + * + * @param {(INavigatable & { appDataEntityId?: string })} XDataModel + * @param {IData} data + * @param {IContext} context + * @param {IParams} params + * @return {*} {{ context: IContext; params: IParams }} + */ +function prepareNavParams( + XDataModel: INavigatable & { appDataEntityId?: string }, + data: IData, + context: IContext, + params: IParams, +): { context: IContext; params: IParams } { + const { + navDER, + navFilter, + navigateContexts, + navigateParams, + appDataEntityId, + } = XDataModel; + const model = { + deName: appDataEntityId ? calcDeCodeNameById(appDataEntityId) : undefined, + navFilter, + pickupDEFName: (navDER as IDER1N)?.pickupDEFName, + navContexts: navigateContexts, + navParams: navigateParams, + }; + const originParams = { + context, + params, + data, + }; + const { resultContext, resultParams } = calcNavParams(model, originParams); + const tempContext = Object.assign(context.clone(), resultContext); + const tempParams = { ...resultParams }; + return { context: tempContext, params: tempParams }; +} + +/** + * 获取树节点模型 + * + * @param {string} nodeId + * @param {IDETree} model + * @return {*} {(IDETreeNode | undefined)} + */ +function getNodeModel(nodeId: string, model: IDETree): IDETreeNode | undefined { + const { detreeNodes } = model; + let nodeModel: IDETreeNode | undefined; + if (detreeNodes) { + detreeNodes.forEach(node => { + if (node.id === nodeId) { + nodeModel = node; + } + }); + } + return nodeModel; +} + +/** + * 获取有导航视图的节点模型标识集合 + * + * @param {IDETree} model + * @return {*} {string[]} + */ +function getNavNodeModelIds(model: IDETree): string[] { + const { detreeNodes } = model; + const navNodeModelIds: string[] = []; + detreeNodes?.forEach(node => { + if (node.navAppViewId) { + navNodeModelIds.push(node.id!); + } + }); + return navNodeModelIds; +} + +/** + * 获取导航视图信息 + * + * @param {(INavigatable & { appDataEntityId?: string })} model + * @param {IData} data + * @param {IContext} context + * @param {IParams} params + * @param {string} viewModelId + * @param {string} [keyName='srfkey'] + * @return {*} {INavViewMsg} + */ +function getNavViewMsg( + model: INavigatable & { appDataEntityId?: string }, + data: IData, + context: IContext, + params: IParams, + viewModelId: string, + keyName: string = 'srfkey', +): INavViewMsg { + const result = prepareNavParams(model, data, context, params); + return { + key: data[keyName], + context: result.context, + params: result.params, + viewId: viewModelId, + }; +} + +/** + * 部件导航 + * + * @export + * @param {MDControlController} controller + * @return {*} {({ + * style: CSSProperties; + * splitMode: Ref<'horizontal' | 'vertical'>; + * splitValue: Ref; + * showNavView: Ref; + * showNavIcon: Ref; + * navViewMsg: Ref; + * })} + */ +export function useControlNav(controller: MDControlController): { + enableNav: Ref; + style: CSSProperties; + splitMode: Ref<'horizontal' | 'vertical'>; + splitValue: Ref; + showNavView: Ref; + showNavIcon: Ref; + navViewMsg: Ref; +} { + const { navViewPos, navViewShowMode } = + controller.model as IControlNavigatable; + /** + * 启用内置导航 + */ + const enableNav: Ref = ref( + navViewShowMode !== undefined && + ![undefined, 'NONE'].includes('navViewPos'), + ); + /** + * 导航样式 + */ + const style = getNavViewStyle(controller.model); + /** + * 分割值 + */ + const splitValue: Ref = ref(0.5); + /** + * 是否显示导航视图 + */ + const showNavView: Ref = ref(navViewShowMode !== 1); + /** + * 是否显示导航图标 + */ + const showNavIcon: Ref = ref(navViewShowMode !== 2); + /** + * 分割模式 + */ + const splitMode: Ref<'horizontal' | 'vertical'> = ref( + ['BOTTOM', 'ANY_BOTTOM'].includes(navViewPos!) ? 'vertical' : 'horizontal', + ); + /** + * 导航视图信息 + */ + const navViewMsg: Ref = ref(); + /** + * 有导航视图的节点模型标识集合 + */ + const navNodeModelIds: string[] = + controller.model.controlType === 'TREEVIEW' + ? getNavNodeModelIds(controller.model) + : []; + /** + * 导航栈数据 + */ + const navStack: string[] = []; + + /** + * 导航数据改变 + * + * @param {IMDControlEvent['onNavDataChange']['event']} event + */ + const onNavDataChange = ( + event: IMDControlEvent['onNavDataChange']['event'], + ) => { + const { navData, context, params } = event; + let keyName = 'srfkey'; + // 设置导航视图信息 + if (['GRID', 'DATAVIEW', 'LIST'].includes(controller.model.controlType!)) { + const viewModelId = (controller.model as INavigatable).navAppViewId; + if (viewModelId) { + navViewMsg.value = getNavViewMsg( + controller.model, + navData, + context, + params, + viewModelId, + keyName, + ); + } + } else if (controller.model.controlType === 'TREEVIEW') { + keyName = '_id'; + const nodeModel = getNodeModel(navData._nodeId, controller.model); + if (!nodeModel) { + throw new RuntimeError( + ibiz.i18n.t('runtime.controller.control.expBar.noFindNodeModel', { + nodeId: navData._nodeId, + }), + ); + } + if (nodeModel.navAppViewId) { + const deData = navData._deData || navData; + navViewMsg.value = getNavViewMsg( + nodeModel, + deData, + context, + params, + nodeModel.navAppViewId, + keyName, + ); + } + } + // 添加栈数据 + navStack.unshift(navData[keyName]); + }; + + /** + * 设置默认导航 + * + */ + const setDefaultNavData = () => { + const { items } = controller.state; + const setNavData = (data: IData | undefined) => { + if (data) { + controller.setSelection([data]); + controller.setNavData(data); + } else { + navViewMsg.value = undefined; + } + }; + const getStackData = (keyName: string = 'srfkey') => { + const preNav = navStack.find(nav => + items.find(item => nav === item[keyName]), + ); + return preNav ? items.find(item => preNav === item[keyName]) : items[0]; + }; + + if (['GRID', 'DATAVIEW', 'LIST'].includes(controller.model.controlType!)) { + const stack = getStackData(); + setNavData(stack); + } else if (controller.model.controlType === 'TREEVIEW') { + const data = controller.state.items.find(node => { + return navNodeModelIds.includes(node._nodeId); + }); + setNavData(data); + } + }; + + controller.evt.on('onNavDataChange', evt => { + if (enableNav.value) { + onNavDataChange(evt); + } + }); + + controller.evt.on('onLoadSuccess', () => { + if (enableNav.value) { + setDefaultNavData(); + } + }); + + controller.evt.on('onRemoveSuccess', () => { + if (enableNav.value) { + setDefaultNavData(); + } + }); + + return { + enableNav, + style, + splitMode, + splitValue, + showNavView, + showNavIcon, + navViewMsg, + }; +} diff --git a/src/common/index.ts b/src/common/index.ts index af17f22a1fd4c2603b9fc0678a73f242b202e04c..c95c3a1211bde74f0c74573c072241ff3bd1c4f5 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -36,6 +36,7 @@ import { IBizPqlEditor } from './pql-editor/pql-editor'; import { IBizCustomFilterCondition } from './custom-filter-condition/custom-filter-condition'; import { IBizAnchorContainer } from './anchor-container/anchor-container'; import { IBizButtonList } from './button-list/button-list'; +import { IBizControlNavigation } from './control-navigation/control-navigation'; export * from './col/col'; export * from './row/row'; @@ -90,6 +91,7 @@ export const IBizCommonComponents = { v.component(IBizCustomFilterCondition.name, IBizCustomFilterCondition); v.component(IBizAnchorContainer.name, IBizAnchorContainer); v.component(IBizButtonList.name, IBizButtonList); + v.component(IBizControlNavigation.name, IBizControlNavigation); }, }; diff --git a/src/control/app-menu/app-menu.scss b/src/control/app-menu/app-menu.scss index 87b8e29c011a5e9a882b2ea6dc1513c9e85f08fe..788d746c7ba6a8f9d5d24a456aecf720c0408d5f 100644 --- a/src/control/app-menu/app-menu.scss +++ b/src/control/app-menu/app-menu.scss @@ -39,7 +39,8 @@ $control-appmenu-item: ( // 垂直菜单项样式 @mixin menu-item-vertical-style { @include flex(row, flex-start, center); - + --el-menu-item-height: #{getCssVar('control-appmenu-item', 'height')}; + --el-menu-sub-item-height: #{getCssVar('control-appmenu-item', 'height')}; --el-menu-base-level-padding: #{getCssVar('control-appmenu-item', 'padding')}; width: 100%; @@ -126,6 +127,12 @@ $control-appmenu-item: ( @include set-component-css-var('control-appmenu', $control-appmenu); @include set-component-css-var('control-appmenu-item', $control-appmenu-item); + @include e(separator) { + @include m(span-mode) { + flex-grow: 1; + } + } + >.el-menu { height: 100%; padding: getCssVar(spacing, none) getCssVar(spacing, tight); @@ -153,6 +160,8 @@ $control-appmenu-item: ( // 垂直 .el-menu--vertical { width: 100%; + display: flex; + flex-direction: column; // 菜单项样式 .el-menu-item, @@ -163,6 +172,7 @@ $control-appmenu-item: ( // 水平 .el-menu--horizontal { + display: flex; & > * + *{ padding-right: getCssVar(spacing, base); } diff --git a/src/control/app-menu/app-menu.tsx b/src/control/app-menu/app-menu.tsx index db8d9c4baaaf1ec4e22cce5889ce066e26d9d37a..d59b639b03318806426a61a58ea5f0f62594bc87 100644 --- a/src/control/app-menu/app-menu.tsx +++ b/src/control/app-menu/app-menu.tsx @@ -45,6 +45,7 @@ function getMenus(items: IAppMenuItem[]): IData[] { disabled: !item.appFuncId, tooltip: item.tooltip, itemType: item.itemType, + spanMode: item.spanMode, sysCss: item.sysCss, }; if (item.appMenuItems?.length) { @@ -196,6 +197,12 @@ function renderMenuItem( ); } if (menu.itemType === 'SEPERATOR') { + const spanModeItem = c.model.appMenuItems?.find( + menuItem => menuItem.itemType === 'SEPERATOR' && menuItem.spanMode, + ); + if (menu.key === spanModeItem?.id) { + return
; + } const direction = c.view.model.mainMenuAlign === 'TOP' && isFirst ? 'vertical' diff --git a/src/control/grid/grid/grid.tsx b/src/control/grid/grid/grid.tsx index fd9cb197e2df3963a345c68f65f57142e3c8607c..5f88c0655728fc64ba1f6d99b5fc509f446b9db7 100644 --- a/src/control/grid/grid/grid.tsx +++ b/src/control/grid/grid/grid.tsx @@ -410,10 +410,9 @@ export const GridControl = defineComponent({ controller={this.c} style={this.headerCssVars} > - { + - } + {this.c.model.enableCustomized && !this.c.state.hideHeader && ( +
+ +
+ )} + {this.renderBatchToolBar()} +
{this.c.state.enablePagingBar && ( )} - {this.c.model.enableCustomized && !this.c.state.hideHeader && ( -
- -
- )} - {this.renderBatchToolBar()} ); },