diff --git a/CHANGELOG.md b/CHANGELOG.md index eee18052fb3f59d4c30e8b04ea3587fe5181eb3f..b6849f39969ef3656dd706552e7e099e7111fca3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ ## [Unreleased] +- 更新侧边栏收缩时支持显示图标 +- 更新菜单自定义设计 ## [0.7.17-alpha.0] - 2024-05-23 ### Changed diff --git a/src/control/app-menu/app-menu.scss b/src/control/app-menu/app-menu.scss index dbd10757763bb21d31d787cc4b3f2e40ce8f6e25..b63961b6f007ccb6c06791f47e7c4e46a1f48211 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 4379c7c6c6db7cfbdcac755bce77be5d8ac80a58..3030ca083e8bb8393dfc272ccb8c7358442d1b53 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(''); // 默认展开菜单项数组 @@ -288,6 +370,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 +447,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 +471,11 @@ export const AppMenuControl = defineComponent({ defaultOpens, menuMode, counterData, + saveConfigs, + configSaves, + configReset, isShowCollapse, + enableCustomized, }; }, render() { @@ -382,6 +486,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 +503,50 @@ 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, + }, + }, + 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 ( +
+
+ {ibiz.i18n.t('control.menuDesign.customMenu')} +
+
+ + {ibiz.i18n.t('control.menuDesign.reset')} + + + {ibiz.i18n.t('control.menuDesign.save')} + +
+
+ ); + }; + + // 绘制菜单内容 + 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 1e464355da3b51b66c7d6ddc62fbf6db7d37a5ed..4ccb17d7e4e72ffad2e113ddfb6d06830a3bae94 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 c4b6303a0b26badff8c9460971a127969a609d35..d51102dd7e8a194067f3d871a51da41de6f58c86 100644 --- a/src/locale/en/index.ts +++ b/src/locale/en/index.ts @@ -115,6 +115,12 @@ export default { noFoundModel: 'no find the menu item model {menuKey}', noFoundFunction: 'The adapter for {menuKey} does not have a renderText method', + menuSetting: '菜单设置', + }, + menuDesign: { + customMenu: 'Custom menu', + reset: 'Reset', + save: 'Save', }, calendar: { lastYear: 'Last year', diff --git a/src/locale/zh-CN/index.ts b/src/locale/zh-CN/index.ts index fce12172c549cb9e4c3190d1faffb26c5b947316..70c6f482bfe124395d5f54aaa23faed500795696 100644 --- a/src/locale/zh-CN/index.ts +++ b/src/locale/zh-CN/index.ts @@ -108,6 +108,12 @@ export default { noSupportAlign: '暂未支持菜单方向为 {align}', noFoundModel: '没找到菜单项模型{menuKey}', noFoundFunction: '{menuKey}的适配器没有renderText方法', + menuSetting: '菜单设置', + }, + menuDesign: { + customMenu: '自定义菜单', + reset: '恢复默认', + save: '保存', }, calendar: { lastYear: '去年', diff --git a/src/panel-component/auth-userinfo/auth-userinfo.scss b/src/panel-component/auth-userinfo/auth-userinfo.scss index 637c8e647a28ea170478d8fe3ca42ae3e3d87146..2a8467bc218f1abc722c67a3b6ba5d644b497926 100644 --- a/src/panel-component/auth-userinfo/auth-userinfo.scss +++ b/src/panel-component/auth-userinfo/auth-userinfo.scss @@ -21,6 +21,9 @@ $user-info: ( @include e(down){ position: relative; top: 12px; + @include when(collapse){ + display: none; + } } } @include when(top){ @@ -37,10 +40,9 @@ $user-info: ( .el-dropdown{ padding: 0 getCssVar(spacing, base-tight); } - @include b(user-info-avatar) { - max-width: 32px; - max-height: 32px; + max-width: 24px; + max-height: 24px; margin: 0; } } @@ -57,6 +59,9 @@ $user-info: ( @include flex(row, space-between, center); color: getCssVar(user-info, color); + @include when('collapse'){ + justify-content: center + } } @include b(user-info-name) { diff --git a/src/panel-component/auth-userinfo/auth-userinfo.tsx b/src/panel-component/auth-userinfo/auth-userinfo.tsx index 8a9eccbe936bfc59ed0b2cd0fe344594435009cf..a063b70db69fdea9dc09a27891db34874a45393d 100644 --- a/src/panel-component/auth-userinfo/auth-userinfo.tsx +++ b/src/panel-component/auth-userinfo/auth-userinfo.tsx @@ -69,7 +69,12 @@ export const AuthUserinfo = defineComponent({ {{ default: (): VNode => ( -
+
diff --git a/src/panel-component/panel-app-title/panel-app-title.controller.ts b/src/panel-component/panel-app-title/panel-app-title.controller.ts index 35bc8a04a1a2933535f432ed970330ff46a315a7..ff46a8706eaac0e50a1a22fc171ceb15c9bb32f5 100644 --- a/src/panel-component/panel-app-title/panel-app-title.controller.ts +++ b/src/panel-component/panel-app-title/panel-app-title.controller.ts @@ -48,8 +48,10 @@ export class PanelAppTitleController extends PanelItemController { if (indexViewModel.title) { document.title = indexViewModel.title; } - // 图标路径 - if (indexViewModel.appIconPath) { + if (this.model.sysImage && this.model.sysImage.rawContent) { + this.state.icon = this.model.sysImage.rawContent; + } else if (indexViewModel.appIconPath) { + // 图标路径 this.state.icon = indexViewModel.appIconPath; } diff --git a/src/panel-component/panel-app-title/panel-app-title.scss b/src/panel-component/panel-app-title/panel-app-title.scss index 5ee86d42ef26899ca8b2e9cb815154d9afb1eb6e..684c084f2125353874aa9b91bb2457c3862698bf 100644 --- a/src/panel-component/panel-app-title/panel-app-title.scss +++ b/src/panel-component/panel-app-title/panel-app-title.scss @@ -33,8 +33,8 @@ $panel-app-title: ( img { display: inline-block; - width: 20px; - height: 20px; + width: auto; + height: 100%; } svg { @@ -57,6 +57,13 @@ $panel-app-title: ( width: 100%; } + @include e(collpase-icon){ + img{ + width: 100%; + height: 100%; + } + } + @include e(caption2){ overflow: hidden; font-size: getCssVar(font-size, header-4); diff --git a/src/panel-component/panel-app-title/panel-app-title.tsx b/src/panel-component/panel-app-title/panel-app-title.tsx index 86486a3a497d050afff02847a20e0d827685f5af..0e55bb57d56443fa9d398f51fef3f68194d6534c 100644 --- a/src/panel-component/panel-app-title/panel-app-title.tsx +++ b/src/panel-component/panel-app-title/panel-app-title.tsx @@ -81,29 +81,28 @@ export const PanelAppTitle = defineComponent({ this.c.state; let iconVNode = null; const captionNode = {caption}; - if (icon || icon2) { - if (isSvg) { - iconVNode = ( - - ); - } else { - iconVNode = ( - - - - ); - } - } else if (this.menuAlign === 'LEFT') { + if (this.menuAlign === 'LEFT') { if (this.isCollapse) { - iconVNode = ( -
-
{caption2}
-
{subCaption2}
-
- ); + if (icon) { + let tempIcon = null; + if (isSvg) { + tempIcon = ; + } else { + tempIcon = ( + + + + ); + } + iconVNode =
{tempIcon}
; + } else { + iconVNode = ( +
+
{caption2}
+
{subCaption2}
+
+ ); + } } else { iconVNode = ( @@ -134,6 +133,18 @@ export const PanelAppTitle = defineComponent({ ); } + } else if (this.menuAlign === 'TOP') { + if (icon) { + if (isSvg) { + iconVNode = ; + } else { + iconVNode = ( + + + + ); + } + } } let content = null; // 左侧只展示图片