在vxe-table表格中增加右键菜单(contextmenu)

首先,列表页面结构如下:

<script setup lang="ts">
import type {
  PaginationQuery,
  ProductInfoEntity,
  SearchBasicOption,
  UserActivityInfo,
} from '@vben/types';

import type { ContextMenu, VxeGridProps } from '#/adapter/vxe-table';

import { onMounted, reactive, ref } from 'vue';

import { Page, useVbenModal } from '@vben/common-ui';
import { $t } from '@vben/locales';

import { ElCard, ElTag } from 'element-plus';

import { OneButton, OneNotification } from '#/adapter';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  columnHelperStore,
  EntityType,
  ModuleIdentifierType,
  OrderStatusPool,
  productCenterStore,
  stateMachinesStore,
  UserBehavior,
} from '#/store';
import {
  BaseDeleteModal,
  BaseProductListBtn,
  BaseUserFieldSettings,
  ProductInfoEdit,
  SearchOperator,
} from '#/views';

//...

// 右键菜单
const productContextMenus: ContextMenu[] = [
  {
    key: 'add',
    text: 'base.base-info-button.add',
    emit: 'context-add',
  },
  {
    key: 'edit',
    text: 'common.edit',
    disabled: (data) => !data.row,
    emit: 'context-edit',
  },
  {
    key: 'onSale',
    text: 'base.base-info-button.onSale',
    disabled: (data) => !data.row,
    emit: 'context-on-sale',
  },
];



/**
 * 列表配置
 */
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: productState.columnList,
    sortConfig: {
      multiple: true,
    },
    
    //...
  },
  gridEvents,
  //此处将菜单内容传进去
  contextMenuDefs: productContextMenus,
});

const handleAddBtn = () => {};
//...

</script>
<template>
  <Page>
    <-- ... -->
    <div class="main-container">
      <ElCard shadow="never" style="border: none">
        <div class="table-container">
          <Grid
            <-- 此处加上触发的对应函数 -->
            @context-add="handleAddBtn"
            @context-edit="handleEditBtn"
            @context-on-sale="handleOnSaleBtn"
          >
            <template #image></template>
          </Grid>
        </div>
      </ElCard>
    </div>
  </Page>
</template>

项目中使用的vxe-table是已经封装过的,在vxe-table组件中,需要加入内容菜单组件

<script lang="ts" setup>
import type {
  VxeGridDefines,
  VxeGridInstance,
  VxeGridListeners,
  VxeGridPropTypes,
  VxeGridProps as VxeTableGridProps,
  VxeToolbarPropTypes,
} from 'vxe-table';

//...


// 记录最近一次右键目标行,用于确保菜单作用于右键行
const lastContextRow = ref<any>();

const state = props.api?.useStore?.();

//...

// 右键整行
function onNativeContextMenu(ev: MouseEvent) {
  const target = ev.target as HTMLElement | null;
  if (!target) return;
  const tr = target.closest('tr');
  if (!tr) return;
  const node = gridRef.value?.getRowNode?.(tr as HTMLElement);
  const row = (node as any)?.item;
  if (row) {
    lastContextRow.value = row;
    gridRef.value?.setCurrentRow?.(row);
    (
      gridEvents.value?.currentRowChange as VxeGridListeners['currentRowChange']
    )?.({
      $table: gridRef.value as any,
      row: row as any,
      rowIndex: -1,
    } as any);
  }
}

//...

const contextHandlerData = computed(() => {
  const currentRow =
    lastContextRow.value ?? gridRef.value?.getCurrentRecord?.();
  return {
    row: currentRow,
    api: props.api,
    emit,
  };
});

//这里的contextMenuDefs就是传进来的菜单配置
const menusFactory = computed(() => {
  const defs = contextMenuDefs?.value ?? [];
  return buildContextMenus(defs);
});
</script>

<template>
  <div :class="cn('bg-card h-full rounded-md', className)">
    <VbenContextMenu
      :menus="menusFactory as any"
      :handler-data="contextHandlerData"
      :modal="false"
      content-class="z-popup"
    >
      <VxeGrid
        ref="gridRef"
        :class="
          cn(
            'p-2',
            {
              'pt-0': showToolbar && !formOptions,
            },
            gridClass,
          )
        "
        v-bind="options"
        v-on="events"
        @contextmenu.capture="onNativeContextMenu"
      >
        <-- ... -->
      </VxeGrid>
    </VbenContextMenu>
  </div>
</template>

其中VbenContextMenu为封装的上下文菜单,内容如下:

<script setup lang="ts">
import type {
  ContextMenuContentProps,
  ContextMenuRootEmits,
  ContextMenuRootProps,
} from 'radix-vue';

import type { ClassType } from '@vben-core/typings';

import type { IContextMenuItem } from './interface';

import { computed } from 'vue';

import { useForwardPropsEmits } from 'radix-vue';

import {
  ContextMenu,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuTrigger,
} from '../../ui/context-menu';

const props = defineProps<
  ContextMenuRootProps & {
    class?: ClassType;
    contentClass?: ClassType;
    contentProps?: ContextMenuContentProps;
    handlerData?: Record<string, any>;
    itemClass?: ClassType;
    menus: (data: any) => IContextMenuItem[];
  }
>();

const emits = defineEmits<ContextMenuRootEmits>();

const delegatedProps = computed(() => {
  const {
    class: _cls,
    contentClass: _,
    contentProps: _cProps,
    itemClass: _iCls,
    ...delegated
  } = props;

  return delegated;
});

const forwarded = useForwardPropsEmits(delegatedProps, emits);

const menusView = computed(() => {
  return props.menus?.(props.handlerData);
});

function handleClick(menu: IContextMenuItem) {
  if (menu.disabled) {
    return;
  }
  menu?.handler?.(props.handlerData);
}
</script>

<template>
  <ContextMenu v-bind="forwarded">
    <ContextMenuTrigger as-child>
      <slot></slot>
    </ContextMenuTrigger>
    <ContextMenuContent
      :class="contentClass"
      v-bind="contentProps"
      class="side-content z-popup"
    >
      <template v-for="menu in menusView" :key="menu.key">
        <ContextMenuItem
          :class="itemClass"
          :disabled="menu.disabled"
          :inset="menu.inset || !menu.icon"
          class="cursor-pointer"
          @click="handleClick(menu)"
        >
          <component
            :is="menu.icon"
            v-if="menu.icon"
            class="mr-2 size-4 text-lg"
          />

          {{ menu.text }}
          <ContextMenuShortcut v-if="menu.shortcut">
            {{ menu.shortcut }}
          </ContextMenuShortcut>
        </ContextMenuItem>
        <ContextMenuSeparator v-if="menu.separator" />
      </template>
    </ContextMenuContent>
  </ContextMenu>
</template>

最后是buildContextMenus的定义:

import type { Component } from 'vue';

import type { IContextMenuItem } from '@vben-core/shadcn-ui';

import { h } from 'vue';

import { $t } from '@vben/locales';

// 供上下文菜单生成时使用的数据
export interface GridContextMenuData {
  row?: any;
  api?: any;
  emit?: (event: string, payload: any) => void;
}

// 列表页传入的菜单定义接口
export interface ContextMenu {
  /** 唯一键 */
  key: string;
  /** 文本的 i18n key(必填),作为参数传入 $t() */
  text: string;
  /** 图标 class(可选) */
  icon?: string;
  /** 是否可见(布尔或根据行/上下文计算) */
  visible?: ((data: GridContextMenuData) => boolean) | boolean;
  /** 是否禁用(布尔或根据行/上下文计算) */
  disabled?: ((data: GridContextMenuData) => boolean) | boolean;
  /** 触发事件名(通过 emit 触发,默认带 row 作为 payload) */
  emit?: string;
  /** 自定义处理函数(拿到 GridContextMenuData) */
  handler?: (data: GridContextMenuData) => void;
  /** 事件 payload 解析器(默认使用 row) */
  payloadResolver?: (data: GridContextMenuData) => any;
}

// 由 ContextMenu[] 生成 VbenContextMenu 需要的 IContextMenuItem[]
export function buildContextMenus(defs: ContextMenu[]) {
  return (data: GridContextMenuData): IContextMenuItem[] => {
    const items: IContextMenuItem[] = [];
    for (const def of defs ?? []) {
      const isVisible =
        typeof def.visible === 'function' ? def.visible(data) : def.visible;
      if (isVisible === false) continue;
      const disabled =
        typeof def.disabled === 'function' ? def.disabled(data) : def.disabled;
      const payload = def.payloadResolver
        ? def.payloadResolver(data)
        : data?.row;
      const text = $t(def.text);
      const iconComp: Component | undefined = def.icon
        ? { render: () => h('span', { class: def.icon }) }
        : undefined;
      items.push({
        key: def.key,
        text,
        icon: iconComp,
        disabled,
        handler: () => {
          if (def.handler) {
            def.handler(data);
            return;
          }
          if (def.emit && typeof data?.emit === 'function') {
            (data.emit as any)(def.emit, payload);
          }
        },
      });
    }
    return items;
  };
}

现在就可以在列表中使用右键菜单,并进行自定义的配置

<think> 在之前的对话中,用户询问了如何避免vxe-table右键菜单触发点击事件。根据引用内容,用户提到在使用vxe-table时遇到了bug,并展示了一个按钮的代码示例。但用户当前的需求是解决右键菜单触发点击事件的问题。 根据vxe-table的文档和常见问题,避免右键菜单触发点击事件通常可以通过以下几种方式: 1. 使用事件修饰符:在监听右键事件时,阻止默认的上下文菜单和事件冒泡。 2. 在事件处理函数中判断事件类型,如果是右键则阻止默认行为。 3.vxe-table的配置中,可能提供了相关的配置项来禁用右键菜单的点击事件触发。 具体实现: 方法一:在模板中使用事件修饰符 例如,在需要监听右键事件的元素上,使用`@click.right`或`@contextmenu`事件,并添加`.prevent`修饰符来阻止默认的上下文菜单,同时使用`.stop`来阻止事件冒泡。 方法二:在事件处理函数中判断 在事件处理函数中,可以通过判断鼠标事件的button属性(右键为2)来区分左右键,然后决定是否执行点击事件的处理逻辑。 方法三:查阅vxe-table的文档,看是否有配置项可以直接禁用右键菜单的点击事件。 由于引用中并没有直接给出相关代码,这里提供一种通用的解决方案: 假设我们在vxe-table的单元格上使用了右键菜单,我们可以这样处理: 在单元格上监听右键点击事件,并阻止默认行为,同时阻止事件冒泡,这样就不会触发单元格的点击事件。 示例代码: ```html <vxe-table ...> <vxe-table-column ...> <template v-slot="{ row }"> <div @contextmenu.prevent.stop="onRightClick(row, $event)"> {{ row.name }} </div> </template> </vxe-table-column> </vxe-table> ``` 这样,右键点击时只会触发`onRightClick`方法,而不会触发单元格的点击事件。 另外,如果用户已经使用了vxe-table自带的右键菜单事件,可以通过配置`context-menu`事件,并在里面阻止事件冒泡和默认行为。 在vxe-table上配置: ```html <vxe-table @context-menu="onContextMenu" ...> ... </vxe-table> ``` 然后在`onContextMenu`方法中: ```javascript methods: { onContextMenu({ row, column, event }) { event.preventDefault(); event.stopPropagation(); // 自定义右键菜单逻辑 // ... } } ``` 这样也可以避免触发点击事件。 如果用户希望完全禁用右键菜单的点击事件,可以考虑在全局或表格上禁用右键菜单的默认行为,但这样可能会影响其他功能,所以通常采用局部处理。 注意:由于vxe-table版本不同,具体API可能有所差异,请参考官方文档。 如果上述方法不能解决问题,可能需要检查是否有其他事件监听器导致了事件触发。 相关问题: 1. vxe-table如何自定义右键菜单? 2.vxe-table中如何区分左键和右键点击事件? 3. vxe-table右键菜单事件有哪些配置选项? </think> 要避免vxe-table右键菜单触发点击事件,可以通过以下方法实现: ### 解决方法 1. **阻止事件冒泡** 在右键菜单事件中使用 `event.stopPropagation()` 阻止事件冒泡到父元素: ```javascript methods: { handleContextMenu({ row, column, event }) { event.stopPropagation(); // 阻止事件冒泡 // 自定义右键菜单逻辑 } } ``` 2. **使用事件修饰符** 在模板中直接添加Vue事件修饰符: ```html <vxe-table @contextmenu.prevent.stop="handleContextMenu" ></vxe-table> ``` - `.prevent`:阻止默认右键菜单 - `.stop`:阻止事件冒泡 3. **分离点击事件** 在左键点击事件中检查事件类型: ```javascript methods: { handleCellClick({ event }) { if (event.button !== 0) return; // 0=左键, 2=右键 // 仅处理左键点击逻辑 } } ``` ### 完整示例 ```html <template> <vxe-table :data="tableData" @contextmenu.prevent.stop="handleContextMenu" @cell-click="handleCellClick" > <!-- 列定义 --> </vxe-table> </template> <script> export default { methods: { handleContextMenu({ event }) { event.stopPropagation(); // 显示自定义右键菜单 this.showMenu(event); }, handleCellClick({ event }) { if (event.button !== 0) return; // 仅响应左键 // 正常点击逻辑 }, showMenu(event) { // 自定义菜单实现 } } } </script> ``` ### 关键点说明 1. **事件区别** - 左键点击 → `@cell-click` - 右键点击 → `@contextmenu` [^1] 2. **事件对象属性** 通过 `event.button` 区分鼠标按键: - `0`:左键 - `2`:右键 3. **冒泡机制** 浏览器事件默认会向上冒泡,使用 `stopPropagation()` 可中断该过程[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值