Parcourir la source

feat: right-click menu supports multiple levels

vben il y a 4 ans
Parent
commit
f645680a3b

+ 2 - 1
CHANGELOG.zh_CN.md

@@ -3,6 +3,7 @@
 ### ✨ Features
 
 - 全局 loading 添加文本
+- 右键菜单支持多级
 
 ### 🎫 Chores
 
@@ -13,7 +14,7 @@
 - Layout 界面布局样式调整
 - 优化表格渲染性能
 - 表单折叠搜索添图标添加动画
-- routeModule 可以忽略 layou 配置不写。方便配置一级菜单
+- routeModule 可以忽略 layout 配置不写。方便配置一级菜单
 
 ### 🐛 Bug Fixes
 

+ 31 - 23
src/components/ContextMenu/src/index.less

@@ -1,5 +1,28 @@
 @import (reference) '../../../design/index.less';
 
+.item-style() {
+  li {
+    display: inline-block;
+    width: 100%;
+    height: 46px !important;
+    margin: 0 !important;
+    line-height: 46px;
+
+    span {
+      line-height: 46px;
+    }
+
+    > div {
+      margin: 0 !important;
+    }
+
+    &:hover {
+      color: @text-color-base;
+      background: #eee;
+    }
+  }
+}
+
 .context-menu {
   position: fixed;
   top: 0;
@@ -18,32 +41,17 @@
   background-clip: padding-box;
   user-select: none;
 
-  &.hidden {
-    display: none !important;
-  }
+  .item-style();
 
-  &__item {
-    a {
-      display: inline-block;
-      width: 100%;
-      padding: 10px 14px;
+  .ant-divider {
+    margin: 0 0;
+  }
 
-      &:hover {
-        color: @text-color-base;
-        background: #eee;
-      }
+  &__popup {
+    .ant-divider {
+      margin: 0 0;
     }
 
-    &.disabled {
-      a {
-        color: @disabled-color;
-        cursor: not-allowed;
-
-        &:hover {
-          color: @disabled-color;
-          background: unset;
-        }
-      }
-    }
+    .item-style();
   }
 }

+ 35 - 14
src/components/ContextMenu/src/index.tsx

@@ -8,9 +8,13 @@ import {
   unref,
   onUnmounted,
 } from 'vue';
+
 import { props } from './props';
 import Icon from '/@/components/Icon';
+import { Menu, Divider } from 'ant-design-vue';
+
 import type { ContextMenuItem } from './types';
+
 import './index.less';
 const prefixCls = 'context-menu';
 export default defineComponent({
@@ -43,12 +47,13 @@ export default defineComponent({
         top: (body.clientHeight < y + menuHeight ? y - menuHeight : y) + 'px',
       };
     });
+
     function handleAction(item: ContextMenuItem, e: MouseEvent) {
+      state.show = false;
       const { handler, disabled } = item;
       if (disabled) {
         return;
       }
-      state.show = false;
       if (e) {
         e.stopPropagation();
         e.preventDefault();
@@ -61,31 +66,47 @@ export default defineComponent({
 
       const { showIcon } = props;
       return (
-        <span style="display: inline-block; width: 100%;">
+        <span style="display: inline-block; width: 100%;" onClick={handleAction.bind(null, item)}>
           {showIcon && icon && <Icon class="mr-2" icon={icon} />}
           <span>{label}</span>
         </span>
       );
     }
     function renderMenuItem(items: ContextMenuItem[]) {
-      return items.map((item) => {
-        const { disabled, label } = item;
+      return items.map((item, index) => {
+        const { disabled, label, children, divider = false } = item;
 
-        return (
-          <li class={`${prefixCls}__item ${disabled ? 'disabled' : ''}`} key={label}>
-            <a onClick={handleAction.bind(null, item)} style="color:#333;">
-              {renderContent(item)}
-            </a>
-          </li>
+        const DividerComp = divider ? <Divider key={`d-${index}`} /> : null;
+        if (!children || children.length === 0) {
+          return [
+            <Menu.Item disabled={disabled} class={`${prefixCls}__item`} key={label}>
+              {() => [renderContent(item)]}
+            </Menu.Item>,
+            DividerComp,
+          ];
+        }
+        return !state.show ? null : (
+          <Menu.SubMenu key={label} disabled={disabled} popupClassName={`${prefixCls}__popup `}>
+            {{
+              title: () => renderContent(item),
+              default: () => [renderMenuItem(children)],
+            }}
+          </Menu.SubMenu>
         );
       });
     }
     return () => {
       const { items } = props;
-      return (
-        <ul class={[prefixCls, !state.show && 'hidden']} ref={wrapRef} style={unref(getStyle)}>
-          {renderMenuItem(items)}
-        </ul>
+      return !state.show ? null : (
+        <Menu
+          inlineIndent={12}
+          mode="vertical"
+          class={[prefixCls]}
+          ref={wrapRef}
+          style={unref(getStyle)}
+        >
+          {() => renderMenuItem(items)}
+        </Menu>
       );
     };
   },

+ 10 - 1
src/components/Description/src/index.tsx

@@ -23,6 +23,7 @@ export default defineComponent({
         ...unref(propsRef),
       };
     });
+
     const getProps = computed(() => {
       const opt = {
         ...props,
@@ -31,12 +32,14 @@ export default defineComponent({
       };
       return opt;
     });
+
     /**
      * @description: 是否使用标题
      */
     const useWrapper = computed(() => {
       return !!unref(getMergeProps).title;
     });
+
     /**
      * @description: 获取配置Collapse
      */
@@ -49,6 +52,7 @@ export default defineComponent({
         };
       }
     );
+
     /**
      * @description:设置desc
      */
@@ -57,9 +61,11 @@ export default defineComponent({
       const mergeProps = deepMerge(unref(propsRef) || {}, descProps);
       propsRef.value = cloneDeep(mergeProps);
     }
+
     const methods: DescInstance = {
       setDescProps,
     };
+
     emit('register', methods);
 
     // 防止换行
@@ -95,6 +101,7 @@ export default defineComponent({
 
         const width = contentMinWidth;
         return (
+          // @ts-ignore
           <Descriptions.Item label={renderLabel(item)} key={field} span={span}>
             {() =>
               contentMinWidth ? (
@@ -113,13 +120,15 @@ export default defineComponent({
         );
       });
     }
+
     const renderDesc = () => {
       return (
-        <Descriptions class={`${prefixCls}`} {...{ ...attrs, ...unref(getProps) }}>
+        <Descriptions class={`${prefixCls}`} {...{ ...attrs, ...(unref(getProps) as any) }}>
           {() => renderItem()}
         </Descriptions>
       );
     };
+
     const renderContainer = () => {
       const content = props.useCollapse ? renderDesc() : <div>{renderDesc()}</div>;
       // 减少dom层级

+ 3 - 2
src/components/Description/src/useDescription.ts

@@ -10,7 +10,7 @@ export function useDescription(props?: Partial<DescOptions>): UseDescReturnType
   const descRef = ref<DescInstance | null>(null);
   const loadedRef = ref(false);
 
-  function getDescription(instance: DescInstance) {
+  function register(instance: DescInstance) {
     if (unref(loadedRef) && isProdMode()) {
       return;
     }
@@ -18,10 +18,11 @@ export function useDescription(props?: Partial<DescOptions>): UseDescReturnType
     props && instance.setDescProps(props);
     loadedRef.value = true;
   }
+
   const methods: DescInstance = {
     setDescProps: (descProps: Partial<DescOptions>): void => {
       unref(descRef)!.setDescProps(descProps);
     },
   };
-  return [getDescription, methods];
+  return [register, methods];
 }

+ 2 - 0
src/components/Icon/index.tsx

@@ -32,6 +32,7 @@ export default defineComponent({
       const { icon, prefix } = props;
       return `${prefix ? prefix + ':' : ''}${icon}`;
     });
+
     const update = async () => {
       const el = unref(elRef);
       if (el) {
@@ -67,6 +68,7 @@ export default defineComponent({
     });
 
     watch(() => props.icon, update, { flush: 'post' });
+
     onMounted(update);
 
     return () => (

+ 2 - 0
src/components/Menu/src/BasicMenu.tsx

@@ -55,6 +55,7 @@ export default defineComponent({
       }
       return menuState.openKeys;
     });
+
     // menu外层样式
     const getMenuWrapStyle = computed((): any => {
       const { showLogo, search } = props;
@@ -130,6 +131,7 @@ export default defineComponent({
       menuState.selectedKeys = [path];
       emit('menuClick', menu);
     }
+
     function handleMenuChange() {
       const { flatItems } = props;
       if (!unref(flatItems) || flatItems.length === 0) {

+ 2 - 0
src/components/Menu/src/useSearchInput.ts

@@ -48,9 +48,11 @@ export function useSearchInput({
     openKeys = es6Unique(openKeys);
     menuState.openKeys = openKeys;
   }
+
   // 搜索框点击
   function handleInputClick(e: any): void {
     emit('clickSearchInput', e);
   }
+
   return { handleInputChange, handleInputClick };
 }

+ 1 - 0
src/components/Preview/src/index.tsx

@@ -219,6 +219,7 @@ export default defineComponent({
         </div>
       );
     };
+
     const renderIndex = () => {
       if (!unref(getIsMultipleImage)) {
         return null;

+ 3 - 2
src/hooks/web/useWatermark.ts

@@ -3,6 +3,7 @@ import { getCurrentInstance, onBeforeUnmount, ref, Ref, unref } from 'vue';
 const domSymbol = Symbol('watermark-dom');
 
 export function useWatermark(appendEl: Ref<HTMLElement | null> = ref(document.body)) {
+  let func: Fn = () => {};
   const id = domSymbol.toString();
   const clear = () => {
     const domId = document.getElementById(id);
@@ -10,6 +11,7 @@ export function useWatermark(appendEl: Ref<HTMLElement | null> = ref(document.bo
       const el = unref(appendEl);
       el && el.removeChild(domId);
     }
+    window.addEventListener('resize', func);
   };
   const createWatermark = (str: string) => {
     clear();
@@ -45,7 +47,7 @@ export function useWatermark(appendEl: Ref<HTMLElement | null> = ref(document.bo
 
   function setWatermark(str: string) {
     createWatermark(str);
-    const func = () => {
+    func = () => {
       createWatermark(str);
     };
     window.addEventListener('resize', func);
@@ -53,7 +55,6 @@ export function useWatermark(appendEl: Ref<HTMLElement | null> = ref(document.bo
     if (instance) {
       onBeforeUnmount(() => {
         clear();
-        window.addEventListener('resize', func);
       });
     }
   }

+ 2 - 2
src/router/routes/modules/demo/feat.ts

@@ -18,7 +18,7 @@ export default {
     {
       path: '/icon',
       name: 'IconDemo',
-      component: () => import('/@/views/demo/comp/icon/index.vue'),
+      component: () => import('/@/views/demo/feat/icon/index.vue'),
       meta: {
         title: '图标',
       },
@@ -43,7 +43,7 @@ export default {
     {
       path: '/click-out-side',
       name: 'ClickOutSideDemo',
-      component: () => import('/@/views/demo/comp/click-out-side/index.vue'),
+      component: () => import('/@/views/demo/feat/click-out-side/index.vue'),
       meta: {
         title: 'ClickOutSide组件',
       },

+ 0 - 2
src/views/demo/comp/button/index.vue

@@ -7,8 +7,6 @@
       show-icon
     />
 
-    <Alert message="按钮扩展" type="info" show-icon class="mt-4" />
-
     <div class="my-2">
       <h3>success</h3>
       <a-button color="success">成功</a-button>

+ 2 - 2
src/views/demo/comp/click-out-side/index.vue → src/views/demo/feat/click-out-side/index.vue

@@ -1,6 +1,6 @@
 <template>
-  <div class="px-10">
-    <Alert message="点内外部触发事件" show-icon class="mt-4"></Alert>
+  <div class="p-10">
+    <Alert message="点内外部触发事件" show-icon></Alert>
     <ClickOutSide @clickOutside="handleClickOutside" class="flex justify-center mt-10">
       <div @click="innerClick" class="demo-box">
         {{ text }}

+ 42 - 1
src/views/demo/feat/context-menu/index.vue

@@ -3,6 +3,10 @@
     <CollapseContainer title="Simple">
       <a-button type="primary" @contextmenu="handleContext">Right Click on me</a-button>
     </CollapseContainer>
+
+    <CollapseContainer title="Multiple" class="mt-4">
+      <a-button type="primary" @contextmenu="handleMultipleContext">Right Click on me</a-button>
+    </CollapseContainer>
   </div>
 </template>
 <script lang="ts">
@@ -36,7 +40,44 @@
           ],
         });
       }
-      return { handleContext };
+
+      function handleMultipleContext(e: MouseEvent) {
+        createContextMenu({
+          event: e,
+          items: [
+            {
+              label: 'New',
+              icon: 'ant-design:plus-outlined',
+
+              children: [
+                {
+                  label: 'New1-1',
+                  icon: 'ant-design:plus-outlined',
+                  divider: true,
+                  children: [
+                    {
+                      label: 'New1-1-1',
+                      handler: () => {
+                        createMessage.success('click new');
+                      },
+                    },
+                    {
+                      label: 'New1-2-1',
+                      disabled: true,
+                    },
+                  ],
+                },
+                {
+                  label: 'New1-2',
+                  icon: 'ant-design:plus-outlined',
+                },
+              ],
+            },
+          ],
+        });
+      }
+
+      return { handleContext, handleMultipleContext };
     },
   });
 </script>

+ 1 - 2
src/views/demo/comp/icon/index.vue → src/views/demo/feat/icon/index.vue

@@ -12,7 +12,7 @@
       </div>
     </CollapseContainer>
 
-    <CollapseContainer title="IconIfy 组件使用" class="mt-5">
+    <CollapseContainer title="IconIfy 组件使用" class="my-5">
       <div class="flex justify-around flex-wrap">
         <Icon icon="fa-solid:address-book" :size="30" />
         <Icon icon="mdi-light:bank" :size="30" />
@@ -23,7 +23,6 @@
 
     <Alert
       show-icon
-      class="mt-5"
       message="推荐使用Iconify组件"
       description="Icon组件基本包含所有的图标,在下面网址内你可以查询到你想要的任何图标。并且打包只会打包所用到的图标。唯一不足的可能就是需要连接外网进行使用。"
     />