From 11bff3e93067768199bab9bfff37e8eae17d8d92 Mon Sep 17 00:00:00 2001
From: zs <zhousong@weben-smart.com>
Date: 周一, 05 5月 2025 16:33:45 +0800
Subject: [PATCH] 库存信息页面

---
 HIAWms/web/src/components/vue3-context-menu/ContextSubMenu.vue |  555 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 555 insertions(+), 0 deletions(-)

diff --git a/HIAWms/web/src/components/vue3-context-menu/ContextSubMenu.vue b/HIAWms/web/src/components/vue3-context-menu/ContextSubMenu.vue
new file mode 100644
index 0000000..b3daeac
--- /dev/null
+++ b/HIAWms/web/src/components/vue3-context-menu/ContextSubMenu.vue
@@ -0,0 +1,555 @@
+<template>
+  <div
+    :class="'mx-context-menu ' + (options.customClass ? options.customClass : '') + ' ' + globalTheme"
+    :style="{
+      maxWidth: (maxWidth ? solveNumberOrStringSize(maxWidth) : `${constOptions.defaultMaxWidth}px`),
+      minWidth: minWidth ? solveNumberOrStringSize(minWidth) : `${constOptions.defaultMinWidth}px`,
+      maxHeight: overflow && maxHeight > 0 ? `${maxHeight}px` : undefined,
+      zIndex: zIndex,
+      left: `${position.x}px`,
+      top: `${position.y}px`,
+    }"
+    data-type="ContextSubMenu"
+    @click="onSubMenuBodyClick"
+    @wheel="onMouseWhell"
+  >
+    <!--Child menu items-->
+    <div 
+      :class="[ 'mx-context-menu-items' ]"
+      ref="menu"
+      :style="{
+        top: `${scrollValue}px`,
+      }"
+    >
+      <slot>
+        <div v-if="overflow && options.updownButtonSpaceholder" class="mx-context-menu-updown placeholder"></div>
+        <template v-for="(item, i) in items" :key="i" >
+          <ContextMenuSeparator v-if="item.hidden !== true && item.divided === 'up'" />
+          <ContextMenuSeparator v-if="item.hidden !== true && item.divided === 'self'" />
+          <!--Menu Item-->
+          <ContextMenuItem
+            v-else
+            :clickHandler="item.onClick ? (e) => item.onClick!(e) : undefined"
+            :disabled="item.disabled"
+            :hidden="item.hidden"
+            :icon="item.icon"
+            :iconFontClass="item.iconFontClass"
+            :svgIcon="item.svgIcon"
+            :svgProps="item.svgProps"
+            :label="item.label"
+            :customRender="(item.customRender as Function)"
+            :customClass="item.customClass"
+            :checked="item.checked"
+            :shortcut="item.shortcut"
+            :clickClose="item.clickClose"
+            :clickableWhenHasChildren="item.clickableWhenHasChildren"
+            :preserveIconWidth="item.preserveIconWidth !== undefined ? item.preserveIconWidth : options.preserveIconWidth"
+            :showRightArrow="item.children && item.children.length > 0"
+            :hasChildren="item.children && item.children.length > 0"
+            :rawMenuItem="item"
+            @sub-menu-open="item.onSubMenuOpen"
+            @sub-menu-close="item.onSubMenuClose"
+          >
+            <template v-if="item.children && item.children.length > 0" #submenu>
+              <!--Sub menu-->
+              <ContextSubMenu 
+                :items="item.children"
+                :maxWidth="item.maxWidth"
+                :minWidth="item.minWidth"
+                :adjustPosition="item.adjustSubMenuPosition !== undefined ? item.adjustSubMenuPosition : options.adjustPosition"
+                :direction="item.direction !== undefined ? item.direction : options.direction"
+              />
+            </template>
+          </ContextMenuItem>
+          <!--Separator-->
+          <!--Custom render-->
+          <ContextMenuSeparator v-if="item.hidden !== true && (item.divided === 'down' || item.divided === true)" />
+        </template>
+        <div v-if="overflow && options.updownButtonSpaceholder" class="mx-context-menu-updown placeholder"></div>
+      </slot>
+    </div>
+        
+    <!--Scroll button host-->
+    <div 
+      class="mx-context-menu-scroll"
+      ref="scroll"
+    >
+      <!--Updown scroll button-->
+      <div
+        v-show="overflow"
+        ref="upScrollButton"
+        :class="'mx-context-menu-updown mx-context-no-clickable up' + (overflow && scrollValue < 0 ? '' : ' disabled')" 
+        @click="onScroll(false)"
+        @wheel="onMouseWhellMx"
+      >
+        <ContextMenuIconRight />
+      </div>
+      <div 
+        v-show="overflow"
+        :class="'mx-context-menu-updown mx-context-no-clickable down' + (overflow && scrollValue > -scrollHeight ? '' : ' disabled')" 
+        @click="onScroll(true)"
+        @wheel="onMouseWhellMx"
+      >
+        <ContextMenuIconRight />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, inject, nextTick, onMounted, type PropType, provide, ref, toRefs } from 'vue'
+import type { MenuOptions, MenuItem, ContextMenuPositionData, MenuPopDirection } from './ContextMenuDefine'
+import type { GlobalHasSlot, GlobalRenderSlot } from './ContextMenu.vue'
+import { MenuConstOptions } from './ContextMenuDefine'
+import { getLeft, getTop, solveNumberOrStringSize } from './ContextMenuUtils'
+import ContextMenuItem from './ContextMenuItem.vue'
+import ContextMenuSeparator from './ContextMenuSeparator.vue'
+import ContextMenuIconRight from './ContextMenuIconRight.vue'
+
+//The internal info context for menu item
+export interface MenuItemContext {
+  focus: () => void,
+  blur: () => void,
+  showSubMenu: () => boolean,
+  getElement: () => HTMLElement|undefined,
+  isDisabledOrHidden: () => boolean,
+  click: (e: MouseEvent|KeyboardEvent) => void,
+}
+
+//The internal info context for submenu instance
+export interface SubMenuContext {
+  isTopLevel: () => boolean;
+  closeSelfAndActiveParent: () => boolean,
+  openCurrentItemSubMenu: () => boolean,
+  closeCurrentSubMenu: () => void,
+  moveCurrentItemFirst: () => void,
+  moveCurrentItemLast: () => void,
+  moveCurrentItemDown: () => void,
+  moveCurrentItemUp: () => void,
+  focusCurrentItem: () => void,
+  triggerCurrentItemClick: (e: KeyboardEvent|MouseEvent) => void,
+}
+
+//The internal info context for submenu
+export interface SubMenuParentContext {
+  //Props
+  container: HTMLElement;
+  zIndex: number;
+  adjustPadding: { x: number, y: number },
+
+  //Position control
+  getParentWidth: () => number;
+  getParentHeight: () => number;
+  getParentX: () => number;
+  getParentY: () => number;
+  getParentAbsX: () => number;
+  getParentAbsY: () => number;
+  getPositon: () => [number,number];
+
+  //SubMenu mutex
+  addOpenedSubMenu: (closeFn: () => void) => void;
+  closeOtherSubMenu: () => void;
+  closeOtherSubMenuWithTimeOut: () => void;
+  checkCloseOtherSubMenuTimeOut: () => boolean;
+
+  //Item control
+  addChildMenuItem: (item: MenuItemContext, index?: number) => void;
+  removeChildMenuItem: (item: MenuItemContext) => void;
+  markActiveMenuItem: (item: MenuItemContext, updateState?: boolean) => void;
+  markThisOpenedByKeyBoard: () => void;
+  isOpenedByKeyBoardFlag: () => boolean;
+  isMenuItemDataCollectedFlag: () => boolean;
+
+  //Other
+  getSubMenuInstanceContext: () => SubMenuContext|null;
+  getParentContext: () => SubMenuParentContext|null;
+  getElement: () => HTMLElement|null;
+}
+
+/**
+ * Submenu container
+ */
+export default defineComponent({
+  name: 'ContextSubMenu',
+  components: {
+    ContextMenuItem,
+    ContextMenuSeparator,
+    ContextMenuIconRight
+  },
+  props: {
+    /**
+     * Items from options
+     */
+    items: { 
+      type: Object as PropType<Array<MenuItem>>,
+      default: null
+    },
+    /**
+     * Max width for this submenu
+     */
+    maxWidth: {
+      type: [String, Number],
+      default: 0,
+    },
+    /**
+     * Min width for this submenu
+     */
+    minWidth: {
+      type: [String, Number],
+      default: 0,
+    },
+    /**
+     * Specifies should submenu adjust it position 
+     * when the menu exceeds the screen. The default is true
+     */
+    adjustPosition: {
+      type: Boolean,
+      default: true,
+    },
+    /**
+     * Menu direction
+     */
+    direction: {
+      type: String as PropType<MenuPopDirection>,
+      default: 'br',
+    },
+  },
+  setup(props) {
+
+    //#region Injects
+
+    const parentContext = inject('menuContext') as SubMenuParentContext;
+    const options = inject('globalOptions') as MenuOptions;
+    const globalHasSlot = inject('globalHasSlot') as GlobalHasSlot;
+    const globalRenderSlot = inject('globalRenderSlot') as GlobalRenderSlot;
+    const globalTheme = inject('globalTheme') as string;
+
+    //#endregion
+    
+    const { zIndex, getParentWidth, getParentHeight } = parentContext;
+    const { adjustPosition } = toRefs(props);
+
+    const menu = ref<HTMLElement>();
+    const scroll = ref<HTMLElement>();
+    const upScrollButton = ref<HTMLElement>();
+    const openedSubMenuClose = [] as (() => void)[];
+
+    //#region Keyboard control context
+
+    const globalSetCurrentSubMenu = inject('globalSetCurrentSubMenu') as (menu: SubMenuContext|null) => void;
+
+    const menuItems = [] as MenuItemContext[];
+    let currentItem = null as MenuItemContext|null;
+    let leaveTimeout = 0;
+
+    function blurCurrentMenu() {
+      if (currentItem)
+        currentItem.blur();
+    }
+
+    function setAndFocusNotDisableItem(isDown: boolean, startIndex?: number) {
+      if (isDown) {
+        for(let i = startIndex !== undefined ? startIndex : 0; i < menuItems.length; i++) {
+          if (!menuItems[i].isDisabledOrHidden()) {
+            setAndFocusCurrentMenu(i);
+            break;
+          }
+        }
+      } else {
+        for(let i = startIndex !== undefined ? startIndex : (menuItems.length - 1); i >= 0; i--) {
+          if (!menuItems[i].isDisabledOrHidden()) {
+            setAndFocusCurrentMenu(i);
+            break;
+          }
+        }
+      }
+    }
+    function setAndFocusCurrentMenu(index?: number) {
+      if (currentItem)
+        blurCurrentMenu();
+      if (index !== undefined)
+        currentItem = menuItems[Math.max(0, Math.min(index, menuItems.length - 1))];
+      if (!currentItem)
+        return;
+
+      //Focus item
+      currentItem.focus();
+
+      //Scroll to current item
+      if (overflow.value) {
+        const element = currentItem.getElement();
+        if (element) {
+          scrollValue.value = Math.min(Math.max(-scrollHeight.value, -element.offsetTop - element.offsetHeight + maxHeight.value), 0);
+        }
+      }
+    }
+    function onSubMenuBodyClick() {
+      //Mouse click can set current focused submenu 
+      globalSetCurrentSubMenu(thisMenuInsContext);
+    }
+
+    const thisMenuInsContext : SubMenuContext = {
+      isTopLevel: () => parentContext.getParentContext() === null,
+      closeSelfAndActiveParent: () => {
+        const parent = thisMenuContext.getParentContext();
+        if (parent) {
+          parent.closeOtherSubMenu();
+          const conext = parent.getSubMenuInstanceContext()
+          if (conext) {
+            conext.focusCurrentItem();
+            return true;
+          }
+        }
+        return false;
+      },
+      closeCurrentSubMenu: () => thisMenuContext.getParentContext()?.closeOtherSubMenu(),
+      moveCurrentItemFirst: () => setAndFocusNotDisableItem(true),
+      moveCurrentItemLast: () => setAndFocusNotDisableItem(false),
+      moveCurrentItemDown: () => setAndFocusNotDisableItem(true, (currentItem ? (menuItems.indexOf(currentItem) + 1) : 0)),
+      moveCurrentItemUp: () => setAndFocusNotDisableItem(false, (currentItem ? (menuItems.indexOf(currentItem) - 1) : 0)),
+      focusCurrentItem: () => setAndFocusCurrentMenu(),
+      openCurrentItemSubMenu: () => {
+        if (currentItem)
+          return currentItem?.showSubMenu()
+        return false;
+      },
+      triggerCurrentItemClick: (e) => currentItem?.click(e),
+    };
+
+    let isOpenedByKeyBoardFlag = false;
+    let isMenuItemDataCollectedFlag = false;
+
+    //#endregion
+
+    //#region Menu control context
+
+    //provide menuContext for child use
+    const thisMenuContext : SubMenuParentContext = {
+      zIndex: zIndex + 1,
+      container: parentContext.container,
+      adjustPadding: options.adjustPadding as { x: number, y: number } || MenuConstOptions.defaultAdjustPadding,
+      getParentWidth: () => menu.value?.offsetWidth || 0,
+      getParentHeight: () => menu.value?.offsetHeight || 0,
+      getParentX: () => position.value.x,
+      getParentY: () => position.value.y,
+      getParentAbsX: () => menu.value ? getLeft(menu.value, parentContext.container) : 0,
+      getParentAbsY: () => menu.value ? getTop(menu.value, parentContext.container) : 0,
+      getPositon: () => [0,0],
+      addOpenedSubMenu(closeFn: () => void) {
+        openedSubMenuClose.push(closeFn);
+      },
+      closeOtherSubMenu() {
+        openedSubMenuClose.forEach(k => k());
+        openedSubMenuClose.splice(0, openedSubMenuClose.length);
+        globalSetCurrentSubMenu(thisMenuInsContext);
+      },
+      checkCloseOtherSubMenuTimeOut() {
+        if (leaveTimeout) {
+          clearTimeout(leaveTimeout);
+          leaveTimeout = 0;
+          return true;
+        }
+        return false;
+      },
+      closeOtherSubMenuWithTimeOut() {
+        leaveTimeout = setTimeout(() => {
+          leaveTimeout = 0;
+          this.closeOtherSubMenu();
+        }, 200) as unknown as number; //Add a delay, the user will not hide the menu when moving too fast
+      },
+      addChildMenuItem: (item: MenuItemContext, index?: number) => {
+        if (index === undefined)
+          menuItems.push(item);
+        else
+          menuItems.splice(index, 0, item);
+      },
+      removeChildMenuItem: (item: MenuItemContext) => {
+        menuItems.splice(menuItems.indexOf(item), 1);
+      },
+      markActiveMenuItem: (item: MenuItemContext, updateState = false) => {
+        blurCurrentMenu();
+        currentItem = item;
+        if (updateState)
+          setAndFocusCurrentMenu();
+      },
+      markThisOpenedByKeyBoard: () => {
+        isOpenedByKeyBoardFlag = true;
+      },
+      isOpenedByKeyBoardFlag: () => {
+        if (isOpenedByKeyBoardFlag) {
+          isOpenedByKeyBoardFlag = false;
+          return true;
+        }
+        return false;
+      },
+      isMenuItemDataCollectedFlag: () => isMenuItemDataCollectedFlag,
+      getElement: () => menu.value || null,
+      getParentContext: () => parentContext,
+      getSubMenuInstanceContext: () => thisMenuInsContext,
+    };
+    provide('menuContext', thisMenuContext);
+
+    //#endregion
+
+    const scrollValue = ref(0);
+    const scrollHeight = ref(0);
+
+    //Scroll the items
+    function onScroll(down : boolean) {
+      if (down)
+        scrollValue.value = Math.min(Math.max(scrollValue.value - 50, -scrollHeight.value), 0);
+      else 
+        scrollValue.value = Math.min(scrollValue.value + 50, 0);
+    }
+
+    function onMouseWhellMx(e: WheelEvent) {
+      e.preventDefault();
+      e.stopPropagation();
+      onScroll (e.deltaY > 0);
+    }
+    function onMouseWhell(e: WheelEvent) {
+      if (options.mouseScroll) {
+        e.preventDefault();
+        e.stopPropagation();
+        onScroll (e.deltaY > 0);
+      }
+    }
+
+    const overflow = ref(false);
+    const position = ref({ x: 0, y: 0 } as ContextMenuPositionData)
+    const maxHeight = ref(0);
+
+    onMounted(() => {
+      const pos = parentContext.getPositon();
+      position.value = {
+        x: pos[0] ?? options.xOffset ?? 0,
+        y: pos[1] ?? options.yOffset ?? 0,
+      };
+
+      //Mark current item submenu is open
+      globalSetCurrentSubMenu(thisMenuInsContext);
+
+      nextTick(() => {
+        const menuEl = menu.value;
+
+        //adjust submenu position
+        if (menuEl && scroll.value) {
+
+          const { container } = parentContext;
+
+          const parentWidth = getParentWidth?.() ?? 0;
+          const parentHeight = getParentHeight?.() ?? 0;
+
+          const fillPaddingX = typeof parentContext.adjustPadding === 'number' ? parentContext.adjustPadding : (parentContext.adjustPadding?.x ?? 0);
+          const fillPaddingYAlways = typeof parentContext.adjustPadding === 'number' ? parentContext.adjustPadding : (parentContext.adjustPadding?.y ?? 0);
+          const fillPaddingY = parentHeight > 0 ? fillPaddingYAlways : 0;
+
+          const windowHeight = document.documentElement.scrollHeight;
+          const windowWidth = document.documentElement.scrollWidth;
+
+          const avliableWidth = Math.min(windowWidth, container.offsetWidth);
+          const avliableHeight = Math.min(windowHeight, container.offsetHeight);
+
+          let absX = getLeft(menuEl, container), 
+            absY = getTop(menuEl, container);
+          
+          //set x positon
+          if (props.direction.includes('l')) {
+            position.value.x -= menuEl.offsetWidth + fillPaddingX; //left
+          }
+          else if (props.direction.includes('r')) {
+            position.value.x += parentWidth + fillPaddingX; //right
+          }
+          else {
+            position.value.x += parentWidth / 2;
+            position.value.x -= (menuEl.offsetWidth + fillPaddingX) / 2; //center
+          }
+
+          //set y positon
+          if (props.direction.includes('t')) {
+            position.value.y -= menuEl.offsetHeight + fillPaddingYAlways * 2; //top
+          }
+          else if (props.direction.includes('b')) {
+            position.value.y -= fillPaddingYAlways;  //bottom
+          }
+          else {
+            position.value.y -= (menuEl.offsetHeight + fillPaddingYAlways) / 2; //center
+          }
+
+          //Overflow adjust
+          if (adjustPosition.value) {
+            nextTick(() => {
+              absX = getLeft(menuEl, container);
+              absY = getTop(menuEl, container);
+              
+              const xOverflow = (absX + menuEl.offsetWidth) - (avliableWidth);
+              const yOverflow = (absY + menuEl.offsetHeight + fillPaddingY * 2) - (avliableHeight);
+
+              overflow.value = yOverflow > 0;              
+              scrollHeight.value = menuEl.offsetHeight - avliableHeight + fillPaddingY * 2 /* Padding */;
+
+              if (xOverflow > 0) {//X overflow
+                const ox = parentWidth + menuEl.offsetWidth - fillPaddingX; 
+                const maxSubWidth = absX;
+                if (ox > maxSubWidth)
+                  position.value.x -= maxSubWidth;
+                else
+                  position.value.x -= ox;
+              }
+
+              if (overflow.value) { //Y overflow
+                const oy = yOverflow;
+                const maxSubHeight = absY;
+                if (oy > maxSubHeight)
+                  position.value.y -= maxSubHeight - fillPaddingY;
+                else
+                  position.value.y -= oy - fillPaddingY;
+                maxHeight.value = (avliableHeight - fillPaddingY * 2);
+              } else {
+                maxHeight.value = 0;
+              }
+            });
+          }
+        }
+
+        //Focus this submenu
+        menuEl?.focus({
+          preventScroll: true
+        });
+
+        //Is this submenu opened by keyboard? If yes then select first item
+        if (parentContext.isOpenedByKeyBoardFlag())
+          setAndFocusNotDisableItem(true);
+
+        isMenuItemDataCollectedFlag = true;
+      });
+    });
+
+    return {
+      menu,
+      scroll,
+      options,
+      zIndex,
+      constOptions: MenuConstOptions,
+      scrollValue,
+      upScrollButton,
+      overflow,
+      position,
+      scrollHeight,
+      maxHeight,
+      globalHasSlot,
+      globalRenderSlot,
+      globalTheme,
+      onScroll,
+      onSubMenuBodyClick,
+      onMouseWhell,
+      onMouseWhellMx,
+      solveNumberOrStringSize,
+    }
+  }
+})
+</script>
+
+<style lang="scss">
+@import "./ContextMenu.scss";
+</style>
\ No newline at end of file

--
Gitblit v1.9.3