From e32794698ca2f89fc97f3bcf6aecb2184304988e Mon Sep 17 00:00:00 2001 From: jianglinjun Date: Tue, 21 May 2024 11:38:03 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=E6=9B=B4=E6=96=B0=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E8=8F=9C=E5=8D=95=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/control/app-menu/app-menu.scss | 46 ++- src/control/app-menu/app-menu.tsx | 180 +++++++-- .../custom-menu-design.scss | 131 +++++++ .../custom-menu-design/custom-menu-design.tsx | 355 ++++++++++++++++++ src/control/app-menu/index.ts | 2 + 5 files changed, 682 insertions(+), 32 deletions(-) create mode 100644 src/control/app-menu/custom-menu-design/custom-menu-design.scss create mode 100644 src/control/app-menu/custom-menu-design/custom-menu-design.tsx diff --git a/src/control/app-menu/app-menu.scss b/src/control/app-menu/app-menu.scss index dbd10757..b63961b6 100644 --- a/src/control/app-menu/app-menu.scss +++ b/src/control/app-menu/app-menu.scss @@ -149,7 +149,7 @@ $control-appmenu-item: ( // 水平 .el-menu--horizontal { & > * + *{ - margin-left: 16px; + padding-right: 16px; } align-items: center; @@ -169,12 +169,12 @@ $control-appmenu-item: ( } } - .el-sub-menu__title { - padding-right: getCssVar( - 'control-appmenu-item', - 'horizontal-padding-right' - ); - } + // .el-sub-menu__title { + // padding-right: getCssVar( + // 'control-appmenu-item', + // 'horizontal-padding-right' + // ); + // } // 副菜单激活 > .el-sub-menu.is-active .el-sub-menu__title { @@ -310,7 +310,7 @@ $control-appmenu-item: ( } -// 底部收缩按钮 +// 底部收缩按钮,自定义设置按钮 @include b(control-appmenu){ @include b(control-appmenu-collapse-icon){ position: absolute; @@ -322,11 +322,41 @@ $control-appmenu-item: ( transform: rotate(180deg); } } + @include b('control-appmenu-menu-set'){ + position: absolute; + bottom: var(--ibiz-spacing-tight); + left: var(--ibiz-spacing-base-tight); + font-size: var(--ibiz-font-size-header-5); + cursor: pointer; + @include when(horizontal){ + right: 0; + bottom: 0; + left: unset; + display: flex; + align-items: center; + height: 100%; + } + + } @include when(collapse) { @include b(control-appmenu-collapse-icon){ right: 0; width: 100%; text-align: center; } + @include b('control-appmenu-menu-set'){ + position: absolute; + right: var(--ibiz-spacing-base-tight); + bottom: calc(var(--ibiz-spacing-tight) + 32px); + font-size: var(--ibiz-font-size-header-5); + text-align: center; + cursor: pointer; + + } + @include when(show-menu-design){ + >.el-menu { + height: calc(100% - getCssVar(control-appmenu, collapse-height) - getCssVar(control-appmenu, collapse-height)); + } + } } } diff --git a/src/control/app-menu/app-menu.tsx b/src/control/app-menu/app-menu.tsx index 4379c7c6..e890815d 100644 --- a/src/control/app-menu/app-menu.tsx +++ b/src/control/app-menu/app-menu.tsx @@ -20,6 +20,7 @@ import { } from '@ibiz-template/runtime'; import { useRoute } from 'vue-router'; import './app-menu.scss'; +import { MenuDesign } from './custom-menu-design/custom-menu-design'; /** * 递归生成菜单数据,递给 element 的 Menu 组件 @@ -75,6 +76,57 @@ function renderByProvider(itemId: string, c: AppMenuController): VNode { return provider.renderText(itemModel, c); } +function findCustomMenu(_key: string, items: IData[]): IData | undefined { + let temp: IData | undefined; + if (items) { + items.some((item: IData): boolean => { + if (item.key === _key) { + temp = item; + return true; + } + if (item.children && item.children.length > 0) { + temp = findCustomMenu(_key, item.children); + if (!temp) { + return false; + } + return true; + } + return false; + }); + } + return temp; +} + +/** + * 获取菜单项自定义配置的禁用状态 + * + * @param {string} _key + * @param {IData[]} items + * @return {*} + */ +function getMenuCustomDisabled(_key: string, items: IData[]) { + const target = findCustomMenu(_key, items); + if (target) { + return target.disabled; + } + return false; +} + +/** + * 获取菜单项自定义配置的显隐 + * + * @param {string} _key + * @param {IData[]} items + * @return {*} + */ +function getMenuCustomVisible(_key: string, items: IData[]) { + const target = findCustomMenu(_key, items); + if (target) { + return target.visible; + } + return true; +} + /** * 绘制菜单项 * @author lxm @@ -89,10 +141,14 @@ function renderMenuItem( ns: Namespace, c: AppMenuController, counterData: IData, + saveConfigs: IData[], ): VNode | undefined { if (!c.state.menuItemsState[menu.key].visible) { return; } + if (!getMenuCustomVisible(menu.key, saveConfigs)) { + return; + } if (menu.itemType === 'MENUITEM') { // eslint-disable-next-line @typescript-eslint/no-explicit-any let content: any; @@ -126,7 +182,7 @@ function renderMenuItem( {content} @@ -141,7 +197,9 @@ function renderMenuItem( {content} @@ -175,23 +233,44 @@ function renderSubmenu( ns: Namespace, c: AppMenuController, counterData: IData, + saveConfigs: IData[], ): VNode | undefined { if (!c.state.menuItemsState[subMenu.key].visible) { return; } + if (!getMenuCustomVisible(subMenu.key, saveConfigs)) { + return; + } return ( {{ default: () => subMenu.children.map((item: IData) => { if (item.children) { - return renderSubmenu(false, item, collapse, ns, c, counterData); + return renderSubmenu( + false, + item, + collapse, + ns, + c, + counterData, + saveConfigs, + ); } - return renderMenuItem(false, item, collapse, ns, c, counterData); + return renderMenuItem( + false, + item, + collapse, + ns, + c, + counterData, + saveConfigs, + ); }), title: () => { const provider = c.itemProviders[subMenu.key]; @@ -239,6 +318,9 @@ export const AppMenuControl = defineComponent({ const c = useControlController((...args) => new AppMenuController(...args)); const ns = useNamespace(`control-${c.model.controlType!.toLowerCase()}`); const menus = ref(getMenus(c.model.appMenuItems!)); + + const saveConfigs = ref([]); + // 默认激活菜单项 const defaultActive = ref(''); // 默认展开菜单项数组 @@ -249,6 +331,9 @@ export const AppMenuControl = defineComponent({ let counter: AppCounter | null = null; const counterData = ref({}); + // 显示设置界面 + const visibleSetting = ref(false); + const key = ref(createUUID()); // 计算当前路由匹配菜单 @@ -288,6 +373,10 @@ export const AppMenuControl = defineComponent({ counterData.value = data; }; + c.evt.on('onCreated', async () => { + saveConfigs.value = c.saveConfigs; + }); + c.evt.on('onMounted', async () => { const allItems = c.getAllItems(); // 默认激活的菜单项 @@ -361,6 +450,20 @@ export const AppMenuControl = defineComponent({ return false; }); + const enableCustomized = computed(() => { + return c.model.enableCustomized; + }); + + // 保存自定义配置 + const configSaves = (saveConfig: IData[]) => { + saveConfigs.value = saveConfig; + }; + + // 自定义配置恢复默认 + const configReset = () => { + saveConfigs.value = []; + }; + return { menus, c, @@ -371,7 +474,12 @@ export const AppMenuControl = defineComponent({ defaultOpens, menuMode, counterData, + saveConfigs, + configSaves, + configReset, isShowCollapse, + enableCustomized, + visibleSetting, }; }, render() { @@ -382,6 +490,7 @@ export const AppMenuControl = defineComponent({ this.ns.m(this.menuMode), this.ns.is('collapse', this.collapse), this.ns.is('show-collapse', this.isShowCollapse), + this.ns.is('show-menu-design', this.enableCustomized), ]} controller={this.c} > @@ -398,28 +507,51 @@ export const AppMenuControl = defineComponent({ mode={this.menuMode} ellipsis={this.menuMode === 'horizontal'} > - {this.menus.map(item => { - if (item.children?.length > 0) { - return renderSubmenu( - true, - item, - this.collapse, - this.ns, - this.c, - this.counterData, - ); - } - return renderMenuItem( - true, - item, - this.collapse, - this.ns, - this.c, - this.counterData, - ); - })} + {{ + default: () => { + return this.menus.map(item => { + if (item.children?.length > 0) { + return renderSubmenu( + true, + item, + this.collapse, + this.ns, + this.c, + this.counterData, + this.saveConfigs, + ); + } + return renderMenuItem( + true, + item, + this.collapse, + this.ns, + this.c, + this.counterData, + this.saveConfigs, + ); + }); + }, + }} )} + {this.enableCustomized && ( + + )} {this.isShowCollapse && (
item.id === itemId); + if (!itemModel) { + throw new RuntimeError(`没找到菜单项模型${itemId}`); + } + const provider = c.itemProviders[itemId]; + if (!provider.renderText) { + throw new RuntimeError(`${itemId}的适配器没有renderText方法`); + } + return provider.renderText(itemModel, c); +} + +/** + * 处理菜单自定义配置 + * + * @param {AppMenuController} c + * @param {IData[]} items + * @return {*} + */ + +export const MenuDesign = defineComponent({ + name: 'IBizMenuDesign', + props: { + controller: { + type: Object as PropType, + required: true, + }, + menus: { + type: Array as PropType, + required: true, + }, + visible: { + type: Boolean, + default: false, + }, + }, + emits: ['saved', 'reset'], + setup(props, { emit }) { + const ns = useNamespace(`menu-design`); + const c = props.controller; + + const loading = ref(false); + + const visible = ref(false); // 抽屉是否显示 + + const configs = ref([]); // 合并配置后的菜单模型 + + // 点击选择的时候不触发分组的折叠 + const stopExpand = (event: MouseEvent) => { + event.stopPropagation(); + }; + + // 处理菜单自定义配置保存数据 + const handleMenusSaveData = (items: IData[]) => { + const tempItems: IData[] = []; + items.forEach((item: IData) => { + const config: IData = { + key: item.key, + name: item.label, + type: item.itemType, + visible: item.config.visible, + disabled: item.config.disabled, + }; + if (item.children?.length) { + config.children = handleMenusSaveData(item.children); + } + tempItems.push(config); + }); + return tempItems; + }; + + // 折叠分组 + const collapseGroup = (menu: IData) => { + menu.isCollapse = !menu.isCollapse; + }; + + // 平铺配置项 + const flattenConfigs = (items: IData[]): IData[] => { + const result: IData[] = []; + items.forEach(item => { + result.push(item); + if (item.children && item.children.length > 0) { + const tempResult = flattenConfigs(item.children); + result.push(...tempResult); + } + }); + return result; + }; + + // 合并菜单自定义配置 + const mergeMenusConfig = (items: IData[]) => { + let customConfig: IData[] = []; + if (c.saveConfigs && c.saveConfigs.length > 0) { + customConfig = flattenConfigs(c.saveConfigs); + } + return items.map((item: IData) => { + const target = customConfig.find((_item: IData) => { + return _item.key === item.key; + }); + + const data: IData = { + ...item, + isCollapse: true, + config: { + visible: true, + disabled: false, + }, + }; + if (item.children?.length) { + data.children = mergeMenusConfig(item.children); + } + if (target) { + Object.assign(data, { + config: { visible: target.visible, disabled: target.disabled }, + }); + } + return data; + }); + }; + + // 恢复默认 + const onReset = async () => { + c.saveConfigs = []; + await c.customController!.resetCustomModelData(); + configs.value = mergeMenusConfig(cloneDeep(props.menus)); + emit('reset'); + }; + + // 保存配置 + const onSave = async () => { + loading.value = true; + const saveConfig: IData[] = handleMenusSaveData(configs.value); + await c.customController!.saveCustomModelData(saveConfig); + c.saveConfigs = saveConfig; + loading.value = false; + emit('saved', saveConfig); + }; + + // 绘制菜单项 + const renderMenuItem = (menu: IData): VNode | undefined => { + if (!c.state.menuItemsState[menu.key]?.visible) { + return; + } + if (menu.itemType === 'MENUITEM') { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let content: any; + const provider = c.itemProviders[menu.key]; + if (provider && provider.renderText) { + content = renderByProvider(menu.key, c); + } else { + content = [ + menu.image ? ( + + ) : null, + {menu.label}, + ]; + } + return content; + } + if (menu.itemType === 'SEPERATOR') { + const direction = + c.view.model.mainMenuAlign === 'TOP' ? 'vertical' : 'horizontal'; + return ( +
+ +
+ ); + } + }; + + // 绘制分组图标 + const renderGroupIcon = (item: IData) => { + if (item.children && item.children.length > 0) { + if (item.isCollapse) { + return ; + } + return ; + } + return null; + }; + + // 绘制菜单项列表树 + const renderMenuList = (items: IData[]) => { + return items.map((item: IData) => { + const content = renderMenuItem(item); + if (!content) return null; + return ( +
+
+
collapseGroup(item)} + > + {renderGroupIcon(item)} +
+ {content} +
+
+ {item.itemType !== 'SEPERATOR' ? ( +
+ + +
+ ) : null} +
+ {item.children && item.children.length > 0 ? ( +
+ {renderMenuList(item.children)} +
+ ) : null} +
+ ); + }); + }; + + // 弹窗头部 + const renderHeader = () => { + return ( +
+
自定义菜单
+
+ + 恢复默认 + + + 保存 + +
+
+ ); + }; + + // 绘制菜单内容 + const renderContent = () => { + return
{renderMenuList(configs.value)}
; + }; + + // 打开弹窗 + const openDesign = () => { + configs.value = mergeMenusConfig(cloneDeep(props.menus)); + visible.value = true; + }; + + watch( + () => props.visibleSetting, + () => {}, + { + immediate: true, + }, + ); + + return { + ns, + c, + configs, + visible, + loading, + onReset, + renderContent, + renderHeader, + onSave, + openDesign, + }; + }, + + render() { + return ( +
+
+ + + + + +
+ + {{ + default: () => { + return this.renderContent(); + }, + header: () => { + return this.renderHeader(); + }, + }} + +
+ ); + }, +}); diff --git a/src/control/app-menu/index.ts b/src/control/app-menu/index.ts index 1e464355..4ccb17d7 100644 --- a/src/control/app-menu/index.ts +++ b/src/control/app-menu/index.ts @@ -3,10 +3,12 @@ import { App } from 'vue'; import { withInstall } from '@ibiz-template/vue3-util'; import { AppMenuControl } from './app-menu'; import { AppMenuProvider } from './app-menu.provider'; +import { MenuDesign } from './custom-menu-design/custom-menu-design'; export const IBizAppMenuControl = withInstall( AppMenuControl, function (v: App) { + v.component(MenuDesign.name, MenuDesign); v.component(AppMenuControl.name, AppMenuControl); registerControlProvider(ControlType.APP_MENU, () => new AppMenuProvider()); }, -- Gitee From b84bf1a97f66d239db91da438a60cb445de4fca4 Mon Sep 17 00:00:00 2001 From: jianglinjun Date: Tue, 21 May 2024 19:44:09 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E8=87=AA=E5=AE=9A=E4=B9=89=E8=AE=BE=E8=AE=A1=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/control/app-menu/app-menu.tsx | 5 --- .../custom-menu-design.scss | 3 ++ .../custom-menu-design/custom-menu-design.tsx | 41 ++++++++++--------- src/locale/en/index.ts | 6 +++ src/locale/zh-CN/index.ts | 6 +++ 5 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/control/app-menu/app-menu.tsx b/src/control/app-menu/app-menu.tsx index e890815d..3030ca08 100644 --- a/src/control/app-menu/app-menu.tsx +++ b/src/control/app-menu/app-menu.tsx @@ -331,9 +331,6 @@ export const AppMenuControl = defineComponent({ let counter: AppCounter | null = null; const counterData = ref({}); - // 显示设置界面 - const visibleSetting = ref(false); - const key = ref(createUUID()); // 计算当前路由匹配菜单 @@ -479,7 +476,6 @@ export const AppMenuControl = defineComponent({ configReset, isShowCollapse, enableCustomized, - visibleSetting, }; }, render() { @@ -546,7 +542,6 @@ export const AppMenuControl = defineComponent({ ), ]} controller={this.c} - visible={this.visibleSetting} menus={this.menus!} onSaved={this.configSaves} onReset={this.configReset} diff --git a/src/control/app-menu/custom-menu-design/custom-menu-design.scss b/src/control/app-menu/custom-menu-design/custom-menu-design.scss index b7b2017d..7c5bf943 100644 --- a/src/control/app-menu/custom-menu-design/custom-menu-design.scss +++ b/src/control/app-menu/custom-menu-design/custom-menu-design.scss @@ -5,6 +5,9 @@ padding: 20px 0 0; margin-bottom: 0; } + @include e('group-icon'){ + cursor: pointer; + } @include b(menu-design-content){ height: 100%; padding: getCssVar('spacing', 'base'); diff --git a/src/control/app-menu/custom-menu-design/custom-menu-design.tsx b/src/control/app-menu/custom-menu-design/custom-menu-design.tsx index b18f9f79..a40c2548 100644 --- a/src/control/app-menu/custom-menu-design/custom-menu-design.tsx +++ b/src/control/app-menu/custom-menu-design/custom-menu-design.tsx @@ -2,7 +2,7 @@ /* eslint-disable no-unused-vars */ import { AppMenuController } from '@ibiz-template/runtime'; import { useNamespace } from '@ibiz-template/vue3-util'; -import { defineComponent, PropType, ref, VNode, watch } from 'vue'; +import { defineComponent, PropType, ref, VNode } from 'vue'; import './custom-menu-design.scss'; import { RuntimeError } from '@ibiz-template/core'; @@ -47,10 +47,6 @@ export const MenuDesign = defineComponent({ type: Array as PropType, required: true, }, - visible: { - type: Boolean, - default: false, - }, }, emits: ['saved', 'reset'], setup(props, { emit }) { @@ -194,9 +190,22 @@ export const MenuDesign = defineComponent({ const renderGroupIcon = (item: IData) => { if (item.children && item.children.length > 0) { if (item.isCollapse) { - return ; + return ( + + ); } - return ; + return ( + + ); } return null; }; @@ -265,13 +274,15 @@ export const MenuDesign = defineComponent({ const renderHeader = () => { return (
-
自定义菜单
+
+ {ibiz.i18n.t('control.menuDesign.customMenu')} +
- 恢复默认 + {ibiz.i18n.t('control.menuDesign.reset')} - 保存 + {ibiz.i18n.t('control.menuDesign.save')}
@@ -289,14 +300,6 @@ export const MenuDesign = defineComponent({ visible.value = true; }; - watch( - () => props.visibleSetting, - () => {}, - { - immediate: true, - }, - ); - return { ns, c, @@ -315,7 +318,7 @@ export const MenuDesign = defineComponent({ return (