首页
/ Slidev 上下文菜单定制指南:通过 context-menu.ts 扩展与 frontmatter 开关控制右键菜单

Slidev 上下文菜单定制指南:通过 context-menu.ts 扩展与 frontmatter 开关控制右键菜单

2026-09-05 22:37:58作者:昌雅子Ethen

在 Slidev 演示过程中,右键菜单(Context Menu)提供了不依赖键盘即可完成翻页、切换画笔、进入演讲者模式等操作的能力。本篇指南讲解如何通过 ./setup/context-menu.ts 文件向默认菜单追加自定义项(支持图标、禁用态、按场景显隐),以及如何用 frontmatter 中的 contextMenu 配置项按开发/构建模式控制菜单的启用,并基于仓库源码剖析菜单的注册链、事件拦截与渲染机制,帮助你做出既可用又可维护的菜单定制方案。

内置上下文菜单有哪些

在写定制代码之前,先了解 Slidev 默认提供了哪些菜单项。客户端入口在 packages/client/setup/context-menu.ts,其中通过 useNav() 等组合式函数构造了一组默认项:

  • Previous Click / Next Click:前进/后退一个点击动画,分别由 hasPrev / hasNext 决定禁用态;
  • Previous Slide / Next Slide:切换幻灯片,依据 currentPagetotal 禁用;
  • 一条 separator 分隔线;
  • Show/Hide editor:仅开发环境显示(show: __DEV__);
  • Show/Hide drawing toolbar:切换画笔工具栏;
  • Show slide overview:打开幻灯片总览;
  • Enter/Exit Presenter Mode:仅在演讲者模式可用或已处于演讲者模式时出现(依赖构建特性开关 __SLIDEV_FEATURE_PRESENTER__);
  • Enter/Close fullscreen:非嵌入场景下切换全屏。

值得注意的实现细节:模块级变量 items 会缓存计算结果(if (items) return items),整个应用生命周期内菜单项只构建一次;后续用户提供的 setup 函数通过 setups.reduce((items, fn) => fn(items), ...) 依次串联在这份默认列表之后。理解这一点后你就能明白:自定义 setup 的入参 items 正是"默认项"的 ComputedRef,这是所有定制写法的基础。

创建 setup/context-menu.ts 追加自定义菜单项

按官方文档的方式,在项目根目录创建 ./setup/context-menu.ts,写入如下内容即可向菜单追加一个新条目(文档原文示例,可复制使用):

// ./setup/context-menu.ts
import { useNav } from '@slidev/client'
import { defineContextMenuSetup } from '@slidev/types'
import { computed } from 'vue'
import Icon3DCursor from '~icons/carbon/3d-cursor'

export default defineContextMenuSetup((items) => {
  const { isPresenter } = useNav()
  return computed(() => [
    ...items.value,
    {
      small: false,
      icon: Icon3DCursor, // if `small` is `true`, only the icon is shown
      label: 'Custom Menu Item', // or a Vue component
      action() {
        alert('Custom Menu Item Clicked!')
      },
      disabled: isPresenter.value,
    },
  ])
})

要点解读:

  • defineContextMenuSetup 只是一个类型收窄函数。在 packages/types/src/setups.ts 中,ContextMenuSetup 被定义为 (items: ComputedRef<ContextMenuItem[]>) => ComputedRef<ContextMenuItem[]>,而 defineContextMenuSetup = defineSetup<ContextMenuSetup>defineSetup 内部只是 return fn——不改变运行时行为,只为编辑器提供参数推断与校验。
  • 返回值必须是 computed:菜单项依赖 isPresenter 等响应式状态,ComputedRef 的懒求值保证每次打开菜单时 disabled 等都取到最新值(渲染模板中对 currentContextMenu.items.value 的遍历即依赖于此)。
  • 保留默认项的写法是展开 ...items.value 后再追加;如果你希望替换默认项而不是追加,也可以只返回自己的列表——reduce 链允许每个 setup 自由决定下一轮的输入。
  • 图标 Icon3DCursor 来自 ~icons/carbon/3d-cursor,即 unplugin-icons 的 Carbon 图标集;同样可以传 UnoCSS 图标类名字符串(如默认项使用的 'i-carbon:arrow-left')。
  • disabled 绑定到 isPresenter,演示了"按场景禁用条目"的用法:处于演讲者模式时该条目以半透明样式呈现且不可点击(渲染层用 op40 类实现)。

ContextMenuItem 类型结构详解

菜单项的类型定义在 packages/types/src/context-menu.ts,它由"普通选项"与 'separator' 字面量构成联合类型,其中普通选项又按 small 分为两种互斥形态:

字段 类型 / 约束 说明
action () => void 必填,点击后执行的回调
small false(可选)/ true 控制渲染形态:图标按钮或"图标 + 标签"整行项
icon Component | string small: false 时可选;small: true 时必填。字符串按 UnoCSS 图标类名解析,组件则直接 :is 渲染
label small: falsestring | Componentsmall: truestring 整行项可为自定义 Vue 组件;图标按钮项只取字符串,且仅作为 title 悬浮提示显示
disabled boolean 禁用态,渲染为 40% 不透明度且无悬停高亮
show boolean 可选,show ?? true;设为 false 时该项整体不渲染(内置编辑项用它在生产环境隐藏)

从源码结构看,这是一个可辨识联合(discriminated union)small: true 分支强制 icon 必填而 label 只能是字符串,small: false 分支则放宽 labelstring | Component 并让 icon 变为可选。因此当你的菜单项是纯图标按钮时,记得写 small: true,否则 TypeScript 会因 label 缺失组件类型而报错。

分隔线不需要对象,直接在数组中放置字符串 'separator' 即可——渲染模板会将其渲染为一条 w-full my1 border-t 分隔线(见 packages/client/internals/ContextMenu.vue 第 69 行)。

底层实现:注册、拦截与渲染

setup 的加载链

客户端 packages/client/setup/context-menu.ts 通过虚拟模块 #slidev/setups/context-menu 导入用户侧 setup 集合,再用 setups.reduce 把默认 computed 列表逐一交给每个 setup 函数。也就是说,./setup/context-menu.ts 的导出会在应用启动构建菜单时被自动发现并串联进这条 reduce 链,无需任何手动注册。

事件拦截逻辑

右键事件的统一入口在 packages/client/logic/contextMenu.tsonContextMenu 函数按顺序做了四道守卫:

  1. 模式守卫configs.contextMenu !== true && configs.contextMenu != null && configs.contextMenu !== mode 时直接返回——这就是 frontmatter 中 dev / build 值生效的位置,mode 来自 packages/client/env.ts 的运行模式;
  2. 原生菜单直通ev.shiftKey || ev.defaultPrevented 时放行,即"按住 Shift 再右键"始终唤起浏览器原生菜单,菜单底部也明确提示了这一快捷键;
  3. 嵌入模式守卫isEmbedded 为真时不弹出自定义菜单(嵌入预览场景由宿主接管交互);
  4. 通过全部守卫后调用 openContextMenu(ev.pageX, ev.pageY),把坐标与 setupContextMenu() 返回的 items ref 存入 currentContextMenu 这个 shallowRef,并 preventDefault() + stopPropagation()

渲染与关闭行为

packages/client/internals/ContextMenu.vue 负责呈现:菜单是 fixed 定位的毛玻璃面板,通过 left / top 计算属性做视口边界钳制(右缘/下缘溢出时自动收回)。small 项渲染为 40x40 的图标按钮并带 title 提示;整行项用 grid-cols-[35px_1fr] 布局,label 为组件时用 <component :is> 渲染。关闭菜单有三种途径:点击外部(onClickOutside)、按下鼠标中键(ev.buttons & 2 的 capture 阶段监听)、窗口失焦。

另外有一个从源码结构看值得注意的交互:当 frontmatter 未显式设置 contextMenuisExplicitEnabled 为假)时,菜单底部会额外显示"按住 Shift 右键可打开原生菜单"的提示,并且开发环境下多一个 "Disable custom context menu" 按钮——它通过 firstSlide.update({ frontmatter: { contextMenu: false } }) 热更新第一页 frontmatter,一键把菜单关掉(ContextMenu.vue 第 29-38 行)。

用 frontmatter 的 contextMenu 控制启用范围

菜单的开关由幻灯片 frontmatter(即 slides.md 首段 YAML)中的 contextMenu 字段控制,类型定义在 packages/types/src/frontmatter.ts

---
# 可取值为 boolean、'dev' 或 'build',默认 true
contextMenu: false
---

结合类型与 onContextMenu 的模式守卫

取值 效果
true(或不写,解析默认 null 表示默认行为) 开发与构建环境均启用;不写时菜单底部会保留"Shift + 右键唤起原生菜单"的提示区
false 全局禁用自定义菜单,右键直接走浏览器原生菜单
'dev' 仅开发模式(slidev 启动的 dev server)启用
'build' 仅构建产物(如 slidev export / 部署后页面)启用

注意 docs/custom/index.md 中的 frontmatter 参考表也列出了该项("enable Slidev's context menu, can be boolean, 'dev' or 'build'")。由于拦截发生在 onContextMenu 内,'build' 取值意味着开发者自己演示时没有菜单、而观众看部署版本时有菜单,适合"演示者靠快捷键、观众靠菜单"的分工场景。

验证与常见问题

  • 看不到新菜单项:确认文件位于项目根目录下的 ./setup/context-menu.ts(Slidev 约定 setup 目录),且默认导出为 defineContextMenuSetup 包裹的函数;返回的必须是 computed 而非普通数组,否则 disabled 等状态不会随导航响应。
  • 图标不显示:字符串图标依赖 UnoCSS 图标规则与 ~icons/ 导入二选一,字符串形式需确保对应图标集合(如 Carbon)已在 uno 配置中可用;组件形式则直接用 unplugin-icons 的 Vue 组件导入。
  • 想临时关闭菜单又不改代码:开发模式下直接右键呼出菜单,点击底部的 "Disable custom context menu" 即可,它会替你写入 contextMenu: false
  • 菜单项点击无效:检查是否被 disabled 命中——模板中禁用项虽仍响应点击事件绑定,但 Slidev 用 op40 样式表达不可用;更可靠的做法是在 action 内自行防御,或让 disabled 与真实前置条件保持一致(如示例中绑定 isPresenter)。

小结

Slidev 的上下文菜单定制建立在一条清晰的分层上:类型层(packages/types/src/context-menu.ts 的可辨识联合)约束条目形态,setup 层(defineContextMenuSetup + 虚拟模块 reduce 链)支持函数式扩展,逻辑层(logic/contextMenu.ts)处理模式/Shift/嵌入三道守卫,视图层(internals/ContextMenu.vue)负责渲染、边界钳制与关闭时机。掌握这条链路后,你既能按文档示例快速追加一个带图标、可响应式禁用的自定义条目,也能精确理解 contextMenu: dev | build 在各环境的实际行为。

登录后查看全文
热门项目推荐
相关项目推荐