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)}
+ >
+
+
+ );
+ },
+ }}
+
+ );
+ };
+
+ 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 (
+
+
+
+ {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} 或找不到对应适配器',