From 5582ce58af8a50f15150b3ea4ad1bc33fd4838ea Mon Sep 17 00:00:00 2001 From: jianglinjun Date: Sat, 11 May 2024 18:00:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E8=AE=BE=E8=AE=A1,=E5=8F=AF?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E9=85=8D=E7=BD=AE=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E7=9A=84=E6=98=BE=E9=9A=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + src/control/app-menu/app-menu.scss | 19 + src/control/app-menu/app-menu.tsx | 126 ++++++- .../custom-menu-design.scss | 134 +++++++ .../custom-menu-design/custom-menu-design.tsx | 341 ++++++++++++++++++ src/control/app-menu/index.ts | 2 + src/locale/en/index.ts | 1 + src/locale/zh-CN/index.ts | 1 + 8 files changed, 621 insertions(+), 4 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/CHANGELOG.md b/CHANGELOG.md index 7fa7cce6..9830c344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ## [Unreleased] ### Added +- 新增菜单自定义设计,可自定义配置菜单的显隐 - 新增视图快捷方式打开 ## [0.7.11] - 2024-05-09 diff --git a/src/control/app-menu/app-menu.scss b/src/control/app-menu/app-menu.scss index dbd10757..27da2a5e 100644 --- a/src/control/app-menu/app-menu.scss +++ b/src/control/app-menu/app-menu.scss @@ -209,6 +209,19 @@ $control-appmenu-item: ( margin: getCssVar('control-appmenu', 'icon-margin'); } + @include e('menu-set'){ + position: absolute; + right: getCssVar(spacing, super-loose); + bottom: getCssVar(spacing, tight); + z-index: 100; + font-size: getCssVar(font-size, header-5); + cursor: pointer; + @include when('collapse'){ + bottom: getCssVar(spacing, extra-tight); + left: getCssVar(spacing, extra-tight); + } + } + // 收缩 @include when(collapse) { @include e(icon) { @@ -217,6 +230,7 @@ $control-appmenu-item: ( @include e(item) { @include menu-collapse-item-style; } + @include b(control-appmenu-tooltip) { width: 100%; } @@ -327,6 +341,11 @@ $control-appmenu-item: ( right: 0; width: 100%; text-align: center; + @include when('allow-custom'){ + right: getCssVar(spacing, tight); + text-align:initial + } + } } } diff --git a/src/control/app-menu/app-menu.tsx b/src/control/app-menu/app-menu.tsx index f232f7fc..69f55462 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,10 +233,14 @@ 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 ( 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 +317,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(''); // 默认展开菜单项数组 @@ -288,6 +369,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 +446,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 +470,11 @@ export const AppMenuControl = defineComponent({ defaultOpens, menuMode, counterData, + saveConfigs, + configSaves, + configReset, isShowCollapse, + enableCustomized, }; }, render() { @@ -407,6 +510,7 @@ export const AppMenuControl = defineComponent({ this.ns, this.c, this.counterData, + this.saveConfigs, ); } return renderMenuItem( @@ -416,15 +520,29 @@ export const AppMenuControl = defineComponent({ this.ns, this.c, this.counterData, + this.saveConfigs, ); })} )} + {this.enableCustomized && ( + + )} {this.isShowCollapse && (
{ this.c.view.call(ViewCallTag.TOGGLE_COLLAPSE); 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 new file mode 100644 index 00000000..f5b88c73 --- /dev/null +++ b/src/control/app-menu/custom-menu-design/custom-menu-design.scss @@ -0,0 +1,134 @@ +@include b(menu-design){ + // height: 100%; + @include b(menu-design-menu-set-drawer){ + background-color: white ; + + .el-drawer__header{ + padding: 20px 0 0; + margin-bottom: 0; + } + } + @include b(menu-design-header){ + height: 64px; + padding: 20px; + border-bottom: 1px solid var(--ibiz-color-border); + @include flex(row, space-between, center); + } + @include b(menu-design-caption){ + padding: 0 getCssVar('spacing', 'base'); + @include e(text) { + position: relative; + height: 100%; + padding-left: getCssVar('spacing', 'base'); + font-size: getCssVar('font-size', 'header-5'); + font-weight: getCssVar('font-weight', 'regular'); + color: getCssVar('color', 'text', 0); + + @include flex(row, flex-start, center); + + &::before { + position: absolute; + top: 50%; + left: 0; + width: 4px; + height: 20px; + content: ''; + background-color: getCssVar('color', 'primary'); + border-radius: 2px; + transform: translateY(-50%); + } + } + } + @include b(menu-design-action){ + margin-right: 64px; + } + @include b(menu-design-content){ + height: 100%; + padding: getCssVar('spacing', 'base'); + overflow: auto; + @include e('menu-item'){ + @include m('children'){ + padding-left: 32px; + @include when('collapse'){ + height: 0; + overflow: hidden; + } + } + } + @include e('menu-item-content'){ + display: flex; + align-items: center; + width: 100%; + height: 40px; + padding: 0 getCssVar(spacing, base-tight); + @include when('is-menuitem'){ + &:hover{ + background-color: getCssVar(color, bg,0); + } + } + + @include m('label'){ + display: flex; + flex:1; + align-items: center; + color: getCssVar(color,black); + + i{ + width: 16px; + text-align: center; + } + } + @include m('label-content'){ + display: flex; + flex: 1; + flex-direction: row; + align-items: center; + justify-content: left; + margin-left: 5px; + font-size:getCssVar(font-size, header-6) + } + @include m('checks'){ + display: flex; + flex: 0; + flex-direction: row; + } + } + @include e('menu-seperator'){ + display: flex; + flex-direction: row; + align-items: center; + width: 100%; + } + + // .el-tree { + // .el-tree-node__content { + // height: 40px; + // padding: 0 getCssVar(spacing, base-tight); + // font-size: getCssVar(font-size, header-6); + + // &:hover { + // .#{bem(menu-design, drag-icon)} { + // display: block + // } + + // .el-tree-node__expand-icon { + // opacity: 0; + // } + // } + // } + // } + .#{bem(menu-design, icon)} { + @include flex(row, center, center); + + width: getCssVar(width-icon, large); + height: getCssVar(width-icon, large); + margin-right: getCssVar(spacing, tight); + } + .#{bem(menu-design, drag-icon)} { + position: absolute; + left: getCssVar(spacing, tight); + display: none; + fill: getCssVar(color, primary, hover); + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..0526f2b8 --- /dev/null +++ b/src/control/app-menu/custom-menu-design/custom-menu-design.tsx @@ -0,0 +1,341 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-vars */ +import { AppMenuController } from '@ibiz-template/runtime'; +import { useNamespace } from '@ibiz-template/vue3-util'; +import { defineComponent, PropType, ref, VNode } from 'vue'; + +import './custom-menu-design.scss'; +import { RuntimeError } from '@ibiz-template/core'; +import { cloneDeep } from 'lodash-es'; + +/** + * 用适配器绘制菜单项文本 + * @author lxm + * @date 2023-12-29 03:36:05 + * @param {string} itemId + * @param {AppMenuController} c + * @return {*} {VNode} + */ +function renderByProvider(itemId: string, c: AppMenuController): VNode { + const itemModel = c.allAppMenuItems.find(item => 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, + }, + }, + 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; + }; + + 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()); }, diff --git a/src/locale/en/index.ts b/src/locale/en/index.ts index 25d7ca54..f5f19d19 100644 --- a/src/locale/en/index.ts +++ b/src/locale/en/index.ts @@ -41,6 +41,7 @@ export default { noFoundModel: 'no find the menu item model {menuKey}', noFoundFunction: 'The adapter for {menuKey} does not have a renderText method', + menuDesign: 'Menu Design', }, calendar: { lastYear: 'Last year', diff --git a/src/locale/zh-CN/index.ts b/src/locale/zh-CN/index.ts index 876b6d13..3cfef84a 100644 --- a/src/locale/zh-CN/index.ts +++ b/src/locale/zh-CN/index.ts @@ -40,6 +40,7 @@ export default { noSupportAlign: '暂未支持菜单方向为 {align}', noFoundModel: '没找到菜单项模型{menuKey}', noFoundFunction: '{menuKey}的适配器没有renderText方法', + menuDesign: '菜单设计', }, calendar: { lastYear: '去年', -- Gitee