ContextMenu 上下文菜单
简介
在指定区域右键(或左键点击)弹出的操作菜单,支持图标、禁用项、分隔线、多级子菜单、当前项高亮与整行自定义内容;也可以按坐标或某个锚点元素打开。
代码演示
基础使用
用 #reference 放置触发元素,菜单项用 co-context-menu-item 声明。icon 决定左侧图标列(只要有一项带图标,整列都会对齐),divided 在项前加分隔线,disabled 置灰不可点;点击后把该项的 command 抛给 @command。
vue
<template>
<div class="demo">
<co-context-menu @command="onCommand" @open="openCount += 1" @close="closeCount += 1">
<template #reference>
<div class="area">在虚线区域内点击鼠标右键</div>
</template>
<co-context-menu-item icon="bi bi-files" title="复制" command="copy" />
<co-context-menu-item icon="bi bi-clipboard" title="粘贴" command="paste" />
<co-context-menu-item icon="bi bi-scissors" title="剪切" command="cut" />
<co-context-menu-item divided icon="bi bi-trash" title="删除" command="delete" disabled />
<co-context-menu-item icon="bi bi-check2-square" title="全选" command="selectAll" />
</co-context-menu>
<p class="hint">已打开 {{ openCount }} 次 · 已关闭 {{ closeCount }} 次(「删除」为禁用项)</p>
</div>
</template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus';
import { ref } from 'vue';
const openCount = ref(0);
const closeCount = ref(0);
const commandText: Record<string, string> = {
copy: '复制',
paste: '粘贴',
cut: '剪切',
delete: '删除',
selectAll: '全选',
};
const onCommand = (command: string) => {
ElMessage.success(`执行:${commandText[command] ?? command}`);
};
</script>
<style scoped>
.demo {
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-start;
}
.area {
display: flex;
align-items: center;
justify-content: center;
width: 320px;
height: 96px;
padding: 12px;
border: 1px dashed var(--el-border-color);
border-radius: 6px;
color: var(--el-text-color-secondary);
user-select: none;
}
.hint {
margin: 0;
font-size: 12px;
color: var(--el-text-color-secondary);
}
</style>
触发方式
trigger 默认 contextmenu(右键);设为 click 后左键点击也能打开,适合做「更多操作」按钮。
vue
<template>
<co-context-menu trigger="click" @command="onCommand">
<template #reference>
<el-button>左键点击打开</el-button>
</template>
<co-context-menu-item icon="bi bi-pencil" title="重命名" command="rename" />
<co-context-menu-item icon="bi bi-download" title="下载" command="download" />
<co-context-menu-item divided icon="bi bi-trash" title="删除" command="delete" />
</co-context-menu>
</template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus';
const onCommand = (command: string) => {
ElMessage.success(`执行:${command}`);
};
</script>
子菜单
co-context-sub-menu 可嵌套任意层,divided、disabled 与菜单项一致。
vue
<template>
<co-context-menu @command="onCommand">
<template #reference>
<el-button>右键打开(含多级子菜单)</el-button>
</template>
<co-context-menu-item icon="bi bi-clipboard" title="复制" command="copy" />
<co-context-menu-item icon="bi bi-scissors" title="剪切" command="cut" />
<co-context-sub-menu divided icon="bi bi-share" title="分享到">
<co-context-menu-item title="微信" command="ShareAppMessage" />
<co-context-menu-item title="朋友圈" command="ShareTimeline" />
<co-context-sub-menu title="更多方式">
<co-context-menu-item title="复制链接" command="copyLink" />
<co-context-menu-item title="生成图片" command="generateImage" />
<co-context-sub-menu divided title="导出为">
<co-context-menu-item title="PNG 图片" command="exportPng" />
<co-context-menu-item title="PDF 文档" command="exportPdf" />
</co-context-sub-menu>
</co-context-sub-menu>
</co-context-sub-menu>
<co-context-sub-menu divided icon="bi bi-sort-down" title="排序" disabled>
<co-context-menu-item title="按名称" command="sortByName" />
<co-context-menu-item title="按时间" command="sortByTime" />
</co-context-sub-menu>
</co-context-menu>
</template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus';
const onCommand = (command: string) => {
ElMessage.success(`执行:${command}`);
};
</script>
手动打开
调用 open(x, y) 传入坐标,菜单出现在指定位置。
vue
<template>
<div class="demo">
<el-button @click="onClick">在鼠标位置打开</el-button>
<p class="hint">调用 open(x, y) 传入坐标,菜单出现在指定位置。</p>
<co-context-menu ref="contextMenuRef" @command="onCommand">
<co-context-menu-item icon="bi bi-files" title="复制" command="copy" />
<co-context-menu-item icon="bi bi-clipboard" title="粘贴" command="paste" />
</co-context-menu>
</div>
</template>
<script lang="ts" setup>
import { type ContextMenuExpose } from 'cosey/components';
import { ElMessage } from 'element-plus';
import { useTemplateRef } from 'vue';
const contextMenuRef = useTemplateRef<ContextMenuExpose>('contextMenuRef');
const onClick = (event: MouseEvent) => {
contextMenuRef.value?.open(event.clientX, event.clientY);
};
const onCommand = (command: string) => {
ElMessage.success(`执行:${command}`);
};
</script>
<style scoped>
.demo {
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-start;
}
.hint {
margin: 0;
font-size: 12px;
color: var(--el-text-color-secondary);
}
</style>
锚点定位
trigger="manual" 搭配 virtual-ref 可以让菜单贴着某个元素显示:virtual-ref 只需提供 getBoundingClientRect,菜单会跟着元素移动。placement 决定方向,offset 决定间距。
非模态场景建议同时把 lock-scroll 关掉——锁滚动会给页面补滚动条宽度并隐藏溢出,导致锚点坐标变化、菜单和元素一起跳一下。
vue
<template>
<div class="demo">
<div class="row">
<div ref="anchorRef" class="anchor">锚点元素</div>
<el-button @click="openMenu">在锚点右侧打开</el-button>
</div>
<p class="hint">菜单固定在左侧元素旁,元素位置变化时会跟着走。</p>
<co-context-menu
ref="menuRef"
trigger="manual"
:virtual-ref="virtualRef"
placement="right-start"
:offset="8"
:lock-scroll="false"
@command="onCommand"
>
<co-context-menu-item icon="bi bi-pencil" title="编辑" command="edit" />
<co-context-menu-item icon="bi bi-files" title="复制一份" command="duplicate" />
<co-context-menu-item divided icon="bi bi-trash" title="删除" command="delete" />
</co-context-menu>
</div>
</template>
<script lang="ts" setup>
import { type ContextMenuExpose } from 'cosey/components';
import { ElMessage } from 'element-plus';
import { computed, useTemplateRef } from 'vue';
const anchorRef = useTemplateRef<HTMLElement>('anchorRef');
const menuRef = useTemplateRef<ContextMenuExpose>('menuRef');
const virtualRef = computed(() => {
const el = anchorRef.value;
return el ? { getBoundingClientRect: () => el.getBoundingClientRect() } : null;
});
const openMenu = () => {
menuRef.value?.open();
};
const onCommand = (command: string) => {
ElMessage.success(`执行:${command}`);
};
</script>
<style scoped>
.demo {
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-start;
}
.row {
display: flex;
align-items: center;
gap: 12px;
}
.anchor {
display: flex;
align-items: center;
height: 32px;
padding-inline: 12px;
border: 1px dashed var(--el-border-color);
border-radius: 6px;
font-size: 13px;
color: var(--el-text-color-secondary);
user-select: none;
}
.hint {
margin: 0;
font-size: 12px;
color: var(--el-text-color-secondary);
}
</style>
当前项高亮
给菜单项加 active,表示它是当前生效的选择(文字使用主色),常用于对齐方式、标题层级这类单选菜单。
vue
<template>
<div class="demo">
<co-context-menu @command="onSelect">
<template #reference>
<div class="area">
<span class="current">当前对齐:{{ currentLabel }}</span>
<span class="hint">在区域内点击鼠标右键切换</span>
</div>
</template>
<co-context-menu-item
v-for="option in options"
:key="option.value"
:icon="option.icon"
:title="option.title"
:command="option.value"
:active="current === option.value"
/>
</co-context-menu>
<p class="text" :style="{ textAlign: current }">这段文字的对齐方式会跟随菜单里选中的那一项。</p>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
const options = [
{ value: 'left', title: '左对齐', icon: 'bi bi-text-left' },
{ value: 'center', title: '居中对齐', icon: 'bi bi-text-center' },
{ value: 'right', title: '右对齐', icon: 'bi bi-text-right' },
{ value: 'justify', title: '两端对齐', icon: 'bi bi-justify' },
];
const current = ref('left');
const currentLabel = computed(
() => options.find((option) => option.value === current.value)?.title ?? '',
);
const onSelect = (command: string) => {
current.value = command;
};
</script>
<style scoped>
.demo {
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-start;
}
.area {
display: flex;
flex-direction: column;
gap: 4px;
width: 320px;
padding: 12px;
border: 1px dashed var(--el-border-color);
border-radius: 6px;
user-select: none;
}
.current {
font-size: 13px;
color: var(--el-text-color-primary);
}
.hint {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.text {
width: 320px;
margin: 0;
font-size: 13px;
color: var(--el-text-color-regular);
}
</style>
自定义内容
默认插槽会替换整行内容,icon 插槽替换左侧图标。需要连续操作时把 close-on-select 设为 false,点击项本身不会关闭菜单,交给内部控件自己决定何时关闭。
vue
<template>
<div class="demo">
<co-context-menu ref="menuRef" @command="onCommand">
<template #reference>
<div class="area">在虚线区域内点击鼠标右键</div>
</template>
<co-context-menu-item command="copy">
<template #icon>
<co-icon name="bi bi-files" />
</template>
<div class="item">
<span>复制</span>
<span class="kbd">⌘ C</span>
</div>
</co-context-menu-item>
<co-context-menu-item command="paste">
<template #icon>
<co-icon name="bi bi-clipboard" />
</template>
<div class="item">
<span>粘贴</span>
<span class="kbd">⌘ V</span>
</div>
</co-context-menu-item>
<co-context-menu-item divided :close-on-select="false">
<template #icon>
<co-icon name="bi bi-palette" />
</template>
<div class="item">
<span>标签颜色</span>
<span class="swatches">
<button
v-for="color in colors"
:key="color"
class="swatch"
:style="{ backgroundColor: color }"
@click.stop="pick(color)"
/>
</span>
</div>
</co-context-menu-item>
</co-context-menu>
<p class="hint">前两项带快捷键提示;颜色项点了不会自动关菜单,选完颜色才手动关闭。</p>
</div>
</template>
<script lang="ts" setup>
import { type ContextMenuExpose } from 'cosey/components';
import { ElMessage } from 'element-plus';
import { useTemplateRef } from 'vue';
const menuRef = useTemplateRef<ContextMenuExpose>('menuRef');
const colors = ['#f56c6c', '#e6a23c', '#67c23a', '#409eff'];
const onCommand = (command: string) => {
ElMessage.success(`执行:${command}`);
};
const pick = (color: string) => {
ElMessage.success(`标签颜色设为 ${color}`);
menuRef.value?.close();
};
</script>
<style scoped>
.demo {
display: flex;
flex-direction: column;
gap: 8px;
align-items: flex-start;
}
.area {
display: flex;
align-items: center;
justify-content: center;
width: 320px;
height: 96px;
padding: 12px;
border: 1px dashed var(--el-border-color);
border-radius: 6px;
color: var(--el-text-color-secondary);
user-select: none;
}
.item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
}
.kbd {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.swatches {
display: flex;
gap: 6px;
}
.swatch {
width: 14px;
height: 14px;
padding: 0;
border: none;
border-radius: 50%;
cursor: pointer;
}
.hint {
margin: 0;
font-size: 12px;
color: var(--el-text-color-secondary);
}
</style>
API
ContextMenuProps
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| disabled | 是否禁用 | boolean | false |
| trigger | 触发方式:contextmenu 右键打开 / click 左键打开 / manual 由外部调用 open() | string | contextmenu |
| virtual-ref | 定位菜单的虚拟引用元素(锚点),配合 manual / click 使用 | { getBoundingClientRect(): DOMRect } | - |
| placement | 菜单相对锚点的放置位置,仅 virtual-ref 定位时生效 | string | right-start |
| offset | 菜单与锚点之间的偏移量(px) | number | 0 |
| persistent | 关闭时仅隐藏菜单不卸载内容(菜单项子树上挂有待交互浮层时开启) | boolean | false |
| lock-scroll | 打开时锁定页面滚动;锚定在元素上的非模态菜单建议关闭 | boolean | true |
ContextMenuSlots
| 插槽 | 描述 | 属性 |
|---|---|---|
| default | 菜单内容 | - |
| reference | 触发菜单显示的 HTML 元素 | - |
ContextMenuEmits
| 事件 | 描述 | 类型 |
|---|---|---|
| command | 当菜单项被点击时触发,参数是菜单项 command 属性的值 | (value: any) => void |
| open | 上下文菜单显示时触发 | () => void |
| close | 上下文菜单隐藏时触发 | () => void |
ContextMenuExpose
| 属性 | 描述 | 类型 |
|---|---|---|
| open | 打开上下文菜单 | (x?: number, y?: number) => void |
| close | 关闭上下文菜单 | () => void |
不传坐标时使用 virtual-ref 锚点定位。
ContextMenuItemProps
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| command | 派发到 command 回调函数的指令参数 | any | - |
| disabled | 是否禁用 | boolean | false |
| divided | 是否显示分隔符 | boolean | false |
| icon | 自定义图标 | string / Component | - |
| title | 菜单标题 | string | - |
| close-on-select | 点击后是否关闭整个菜单(内嵌下拉/网格等浮层时设为 false) | boolean | true |
| active | 是否为当前激活态(如当前选中的标题层级、列表类型) | boolean | false |
ContextMenuItemSlots
| 插槽 | 描述 | 属性 |
|---|---|---|
| default | 自定义菜单项内容(提供时替换 title 文字,可承载任意组件) | - |
| icon | 自定义图标(覆盖 icon prop,可放置任意图标组件) | - |
ContextSubMenuProps
| 属性 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| disabled | 是否禁用 | boolean | false |
| divided | 是否显示分隔符 | boolean | false |
| icon | 自定义图标 | string / Component | - |
| title | 菜单标题 | string | - |
ContextSubMenuSlots
| 插槽 | 描述 | 属性 |
|---|---|---|
| default | 子菜单内容 | - |
| icon | 自定义图标(覆盖 icon prop,可放置任意图标组件) | - |