diff --git a/CHANGELOG.md b/CHANGELOG.md index 00830bb27bab79c8b690509ba634b09d2aa2fb8f..6e5035b1ecfe13b2731646c48e54d608d0ed52b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ ## [Unreleased] +### Added + +- 新增菜单自定义功能 + ## [0.0.50] - 2025-01-23 ### Added diff --git a/ibiz.config.ts b/ibiz.config.ts index 297ca5f1bfab938ba10318bc9302774e658cc4f4..4a5980624495e9302f7d5e75ba8913a01109e618 100644 --- a/ibiz.config.ts +++ b/ibiz.config.ts @@ -38,6 +38,7 @@ export default defineConfig({ 'ramda', 'lodash-es', 'qx-util', + 'vuedraggable', 'pinia', 'cherry-markdown', '@ibiz-template/mob-theme', diff --git a/package.json b/package.json index 6d99c16d3e386fd9dc054ad9253dcb5fb3b3e4db..c7a212dfe5bb7e501198fd9840899326db030859 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "vue-i18n": "^9.6.5", "vue-router": "^4.2.5", "qr-code-styling": "^1.8.3", + "vuedraggable": "^4.1.0", "vue-qrcode-reader": "5.5.11" }, "devDependencies": { @@ -85,6 +86,7 @@ "ramda": "^0.29.0", "vant": "^4.7.2", "vue": "^3.3.4", + "vuedraggable": "^4.1.0", "vue3-hash-calendar": "^1.1.3", "vue-router": "^4.2.4" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e63b87cee50b2d40dcc0ac1695ddf0e7b42c1e3a..3cb6062efeb471025a57d55170976287b13fbdc4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ dependencies: vue3-hash-calendar: specifier: ^1.1.3 version: 1.1.3(vue@3.3.9) + vuedraggable: + specifier: ^4.1.0 + version: 4.1.0(vue@3.3.9) devDependencies: '@commitlint/cli': diff --git a/src/control/app-menu/app-menu.scss b/src/control/app-menu/app-menu.scss index ebd2271d9dfd9b41f179cd1a006799255485b5ae..0307f6dcdc6a6db070388becc6aaf3a88d1ce65c 100644 --- a/src/control/app-menu/app-menu.scss +++ b/src/control/app-menu/app-menu.scss @@ -8,6 +8,7 @@ .van-tabbar-item__icon { width: var(--van-tabbar-item-icon-size); + font-size: getCssVar(width, icon, medium); } } diff --git a/src/control/app-menu/app-menu.tsx b/src/control/app-menu/app-menu.tsx index 87fb389876d8d36692e5e73ac10253da6aad279a..88c83bfb4006e6e76c55372358fe39b9b4b72f7f 100644 --- a/src/control/app-menu/app-menu.tsx +++ b/src/control/app-menu/app-menu.tsx @@ -1,8 +1,9 @@ import { useRoute } from 'vue-router'; -import { defineComponent, onMounted, PropType, ref, watch } from 'vue'; +import { ref, watch, PropType, onMounted, defineComponent } from 'vue'; import { prepareControl, useControlController } from '@ibiz-template/vue3-util'; import { IAppMenu, IAppMenuItem } from '@ibiz/model-core'; import { AppMenuController, IControlProvider } from '@ibiz-template/runtime'; +import { MenuDesign } from './custom-menu-design/custom-menu-design'; import './app-menu.scss'; export const AppMenuControl = defineComponent({ @@ -17,6 +18,7 @@ export const AppMenuControl = defineComponent({ const c = useControlController((...args) => new AppMenuController(...args)); const { controlClass, ns } = prepareControl(c); const activeName = ref(); + const showPopup = ref(false); // 路由对象 const route = useRoute(); // 计算当前路由匹配菜单 @@ -32,11 +34,16 @@ export const AppMenuControl = defineComponent({ }); }; + const setPopupState = (state: boolean): void => { + showPopup.value = state; + }; + const onTabChange = async ( active: string, event?: MouseEvent, opts: IData = {}, ) => { + if (active === 'customized') return setPopupState(true); activeName.value = active; await c.onClickMenuItem(active, event, true, opts); }; @@ -71,40 +78,17 @@ export const AppMenuControl = defineComponent({ ); return { - controlClass, - activeName, c, - onTabChange, ns, + showPopup, + activeName, + controlClass, + onTabChange, + setPopupState, }; }, render() { const { model, state } = this.c; - let appMenu = null; - if (state.isCreated && model.appMenuItems?.length) { - appMenu = model.appMenuItems.map(item => { - if (item.itemType === 'MENUITEM' && item.hidden !== true) { - const itemState = state.menuItemsState[item.id!]; - if (itemState.visible) { - return ( - - {{ - icon: () => ( - - ), - default: () => {item.caption}, - }} - - ); - } - } - return null; - }); - } return (
- {appMenu} + {state.mobMenuItems.map(item => ( + + {{ + icon: () => ( + + ), + default: () => {item.caption}, + }} + + ))} + {model.enableCustomized && ( + + {{ + icon: () => , + default: () => ( + {ibiz.i18n.t('control.appmenu.more')} + ), + }} + + )} + + this.setPopupState(false)} + /> +
); }, 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 0000000000000000000000000000000000000000..4d5f89a624c0bcf2a5e6642035c7b045275adc10 --- /dev/null +++ b/src/control/app-menu/custom-menu-design/custom-menu-design.scss @@ -0,0 +1,75 @@ +@include b(menu-design) { + width: 100%; + height: 100%; + padding: getCssVar(spacing, base); + background-color: getCssVar(color, fill, 2); + @include e(header) { + display: flex; + align-items: center; + justify-content: space-between; + font-size: getCssVar(font-size, header, 5); + @include m(close-icon) { + font-size: getCssVar(font-size, header, 3); + } + @include m(caption) { + font-weight: getCssVar(font-weight, bold); + } + @include m(save) { + color: getCssVar(color, primary); + } + } + + @include e(group) { + @include m(caption) { + color: getCssVar(color, text, 2); + padding: getCssVar(spacing, base) 0; + } + } + + @include e(draggable) { + padding: getCssVar(spacing, base, tight); + border-radius: getCssVar(border-radius, medium); + background-color: getCssVar(color, bg, 1); + + @include m(icon) { + font-size: getCssVar(font-size, header, 3); + &.#{bem(menu-design, draggable, prefix-icon)} { + margin-right: getCssVar(spacing, tight); + } + &.#{bem(menu-design, draggable, remove-icon)} { + color: getCssVar(color, danger); + } + &.#{bem(menu-design, draggable, add-icon)} { + color: getCssVar(color, primary); + } + } + + @include m(item) { + width: 100%; + display: flex; + align-items: center; + font-size: getCssVar(width, icon, medium); + } + + @include m(item-content) { + flex-grow: 1; + display: flex; + align-items: center; + justify-content: space-between; + padding: getCssVar(spacing, base, tight) 0; + border-bottom: 1px solid getCssVar(color, border); + } + + @include m(item-caption) { + display: flex; + align-items: center; + } + + @include m(item-icon) { + display: flex; + align-items: center; + width: var(--van-tabbar-item-icon-size); + margin-right: getCssVar(spacing, tight); + } + } +} 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 0000000000000000000000000000000000000000..ae03a3b7800a452f25da01289d09cf1a7888e0b1 --- /dev/null +++ b/src/control/app-menu/custom-menu-design/custom-menu-design.tsx @@ -0,0 +1,214 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { AppMenuController } from '@ibiz-template/runtime'; +import { useNamespace } from '@ibiz-template/vue3-util'; +import { IAppMenuItem } from '@ibiz/model-core'; +import draggable from 'vuedraggable'; +import { defineComponent, PropType, ref, watch } from 'vue'; +import './custom-menu-design.scss'; + +/** + * 处理菜单自定义配置 + * + * @param {AppMenuController} c + * @param {IData[]} items + * @return {*} + */ +export const MenuDesign = defineComponent({ + name: 'IBizMenuDesign', + components: { + draggable, + }, + props: { + controller: { + type: Object as PropType, + required: true, + }, + show: { + type: Boolean, + default: false, + }, + }, + emits: ['close'], + setup(props, { emit }) { + const ns = useNamespace('menu-design'); + const c = props.controller; + + // 底部导航菜单 + const mobMenuItems = ref([]); + + // 更多导航菜单 + const moreMenuItems = ref([]); + + const calcMenuItems = () => { + const menuItems = + c.model.appMenuItems?.filter( + item => + item.hidden !== true && + item.itemType === 'MENUITEM' && + c.state.menuItemsState[item.id!].visible, + ) || []; + mobMenuItems.value = [...c.state.mobMenuItems]; + moreMenuItems.value = menuItems.filter( + menu => !c.state.mobMenuItems.find(_item => menu.id === _item.id), + ); + }; + + watch( + () => props.show, + () => { + if (props.show) calcMenuItems(); + }, + { + immediate: true, + }, + ); + + /** + * 关闭 + * + */ + const onClose = () => { + emit('close'); + }; + + /** + * 保存 + * + */ + const onSave = async () => { + await c.customController!.saveCustomModelData(mobMenuItems.value); + c.state.mobMenuItems = mobMenuItems.value; + onClose(); + }; + + /** + * 处理添加或删除 + * + * @param {('nav' | 'more')} type + * @param {number} index + */ + const handleRemoveOrAdd = (type: 'nav' | 'more', index: number) => { + if (type === 'nav') { + const item = mobMenuItems.value[index]; + mobMenuItems.value.splice(index, 1); + moreMenuItems.value.push(item); + } else { + const item = moreMenuItems.value[index]; + moreMenuItems.value.splice(index, 1); + mobMenuItems.value.push(item); + } + }; + + /** + * 绘制拖拽区 + * + * @param {IAppMenuItem[]} menuItems + */ + const renderDraggable = ( + type: 'nav' | 'more', + menuItems: IAppMenuItem[], + ) => { + return ( + + {{ + item: ({ + element, + index, + }: { + element: IAppMenuItem; + index: number; + }) => { + return ( +
+ handleRemoveOrAdd(type, index)} + > +
+
+ +
{element.caption}
+
+ +
+
+ ); + }, + }} +
+ ); + }; + + const renderMenuList = (type: 'nav' | 'more') => { + return ( +
+
+ {type === 'nav' + ? ibiz.i18n.t('control.appmenu.bottomNav') + : ibiz.i18n.t('control.appmenu.more')} +
+ {renderDraggable( + type, + type === 'nav' ? mobMenuItems.value : moreMenuItems.value, + )} +
+ ); + }; + + return { + c, + ns, + onSave, + onClose, + renderMenuList, + }; + }, + + render() { + return ( +
+
+ +
+ {ibiz.i18n.t('control.appmenu.customNav')} +
+
+ {ibiz.i18n.t('control.appmenu.save')} +
+
+
+ {this.renderMenuList('nav')} + {this.renderMenuList('more')} +
+
+ ); + }, +}); diff --git a/src/control/app-menu/index.ts b/src/control/app-menu/index.ts index bf9acc2f802bc09de2b2cf1e3f76620b672a7279..5a9670e7c470abf60f9ab65ddc0a3776a4aa49e2 100644 --- a/src/control/app-menu/index.ts +++ b/src/control/app-menu/index.ts @@ -1,5 +1,5 @@ -import { ControlType, registerControlProvider } from '@ibiz-template/runtime'; import { App } from 'vue'; +import { ControlType, registerControlProvider } from '@ibiz-template/runtime'; import { withInstall } from '@ibiz-template/vue3-util'; import { AppMenuControl } from './app-menu'; import { AppMenuProvider } from './app-menu.provider'; @@ -9,7 +9,7 @@ export * from './app-menu.provider'; export const IBizAppMenuControl = withInstall( AppMenuControl, function (v: App) { - v.component(AppMenuControl.name, AppMenuControl); + 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 c2540e7a2f5c5f9de782c94c17fbb1aa6ee04c75..43301a1b8f8a98edb2954771dae669f8e8caf9fd 100644 --- a/src/locale/en/index.ts +++ b/src/locale/en/index.ts @@ -66,6 +66,12 @@ export default { common: { loadMore: 'Load more', }, + appmenu: { + more: 'More', + bottomNav: 'Bottom Navigation', + customNav: 'Customize Navigation', + save: 'Save', + }, dataView: { end: 'The end~' }, form: { noSupportDetailType: diff --git a/src/locale/zh-CN/index.ts b/src/locale/zh-CN/index.ts index ac826e54b4e59c79c8c2edee9b6cd51ac79aea7e..d3c4b48ca6d6531d9e682e1591a42c3261094853 100644 --- a/src/locale/zh-CN/index.ts +++ b/src/locale/zh-CN/index.ts @@ -62,6 +62,12 @@ export default { loadMore: '加载更多', }, dataView: { end: '我已经到底啦~' }, + appmenu: { + more: '更多', + bottomNav: '底部导航', + customNav: '自定义导航', + save: '保存', + }, form: { noSupportDetailType: '暂未支持的表单项类型: {detailType} 或找不到对应适配器',