当前位置: 首页 > news >正文

Vue3 如何实现一个函数式右键菜单(ContextMenus)

前言: 最近在公司 PC 端的项目中使用到了右键出现菜单选项这样的一个工作需求,并且自己现在也在实现一个偶然迸发的 idea( 想用前端实现一个 windows 系统从开机到桌面的 UI),其中也要用到右键弹出菜单这样的一个功能,个人觉得这个实现还不错,特来分享🎁。

tips: 我个人是喜欢使用图文来讲解知识点的,相比于直接讲概念,我个人更倾向于使用费曼学习法来讲解某一个功能的实现过程,因为我也是刚从一只菜鸟走过来,所以我更加清楚一个新手在去学习一个全新的知识的时候,他其实不是需要你给他讲实现原理,而是你需要作为一个 “引路人” 让他先简单知道这个知识是用来干什么的,后面随着他自己一步一步的深入了解,他会自己慢慢领悟其中的原理。

一. 前期准备

  1. 我们需要清楚的认识到,这种用户点击右键然后弹出菜单的动作行为是非常不适合将组件写死在页面上,然后通过使用 v-show 或者 v-if 去控制它的出现和消失的,我们需要想办法使用函数式去控制它的行为。

  2. 在此之前,你需要准备两个文件来和我一起实现这个右键菜单。
    image.png

  3. 预览图:
    333.gif

二. 右键菜单的样式

  1. 菜单样式的书写不是我们本文的重点,你可以快速在 Menu.vue 里简单书写你自己喜欢的一个简单 div 即可,我们的重点是在于如何右键弹出它。你也可以在下方的源码标题中直接复制我书写的样式,不过你需要使用 UnoCSS 来支持内敛样式属性。

  2. 如果你不知道如何使用 Unocss,你可以参考这篇文章的内容 手把手教你实现一个代码仓库里面有详细的过程来帮助你去完成代码仓库的构建,其中包括了 Unocss 如何引入和使用。)
    image.png

三. h 函数 和 render 函数的使用

  1. 现在我们已经完成了 Menu.vue,文件的内容,接下来我们需要转头去书写 index.ts 内的内容。

  2. 在此之前,我们需要引入两个 vue 暴露给我们的,十分重要的函数。h,和 render
    image.png

  3. 如果你之前读过我另外三篇文章,我相信你对这两个函数的使用一定不陌生,但是为了照顾之前没有了解过的读者,我还是会在接下来的内容中简单介绍一下。不过我还是建议你去看一看下面的实现方式,你一定会有不一样的收获。

  • Vue3 如何实现一个 Toast 小弹窗
  • Vue3 如何实现一个全局搜索框
  • Vue3 如何实现一个Dialog
  1. 接下来我简单的介绍一下,这两个函数的使用方式。你需要知道一个前提知识,我们在 template 标签里书写的样式,最终都会被转变成虚拟 dom
    image.png
    这里面书写的 div 其实是和我们在浏览器里看到的 div “并不是同一个” div,只不过经过 vue 帮我们进行了处理,让它们的表现形式显得一样了。

  2. template 是经过了怎样的处理呢?其实就是经过了 h 函数。然后 h 函数会返回一个特殊的 JS 对象,这个特殊的对象就是我们所说的虚拟dom

  3. 那我们在这个场景怎么使用呢?首先你需要在 index.ts 文件内引入我们刚刚书写的右键菜单的样式。然后将这个组件作为 h 函数的第一个参数放入,对,就是这么简单。这个 vnode 就是我们需要用到的虚拟 dom
    image.png

  4. 有了虚拟 dom 还不行,我们得告诉 vue 我们要把这个虚拟 dom 渲染到什么地方,这时候就需要用到 render 函数。render 函数要做的事情比较复杂,不过在这里你只需要简单的知道。render 函数会将一个 虚拟dom 转换成一个真实的 dom 节点。既然需要一个虚拟 dom,那我刚刚正好用 h 函数转换了得到了一个,于是我们自然而然可以写出下面的代码。
    image.png

  5. 怎么回事?怎么还报错了呢?
    image.png
    我们看一下报错信息,发现这个 render 函数需要两个参数,我们只给了一个。那么第二个参数是什么呢?我们思考一下,现在这个 dom 已经被转换成真实的 dom 节点了,但是目前它不知道自己应该被渲染到哪里,什么意思呢?其实理解起来很简单。
    就好比你现在是一个外卖员,你到了餐厅取餐,餐厅人员说你去吧,你端着手上的一份外卖餐一脸茫然,我去哪啊?
    就对应着,vue 帮你处理好了这个虚拟节点,但是你没告诉它应该在哪里去渲染。

  6. 知道原因就好办了,我们直接创建一个空的 div,先让 render 用着。
    image.png

四. 右键弹出菜单的实现

  1. 在进行下面的功能之前,你需要知道一个前提知识。
    右键.gif
    如上面的 gif 所示,我们可以看到,浏览器本身是存在默认的右键点击事件的。在这里我们需要取消浏览器自身的右键弹出菜单事件。

  2. 我们再具体一点讲,其实我们需要做的就是替换掉浏览器默认的右键事件。通过查阅 MDN 我们可以得知,window 对象存在一个叫做 contextMenus 的事件。
    image.png

  3. 那接下来就好办了,我们直接替换这个事件为我们的自定义事件即可。(这里阻止默认事件需要调用 e.preventDefault 方法。)
    image.png
    然后我们在随便一个全屏的组件引入这个函数,我们来测试一下,看看效果
    2.gif

  4. 嗯,现在已经不会弹出浏览器默认的菜单了。那么接下来要做的就是如何让我们写好的菜单呈现到页面上。首先第一点,我们需要明确告诉这个组件你的父元素是谁
    我们上面只是临时创造了一个简单的 div,但是目前我们还是没告诉它应该渲染到哪里。处理方法也很简单,这里我提前创建好了一个很简单的页面,并且设置好了一个唯一 ID
    image.png

  5. 那么我们就可以非常轻松的获得这个元素。
    image.png

  6. 现在父元素也有了,只需要将我们的 containerEl 元素放入到 scope 里即可。
    不过你需要知道的是,我们这个元素是不应该出现在正常的文档流里的,因为它的位置是不固定的,所以我们在放进去 scope 元素之前,应该给它处理成绝对定位类型的元素。
    image.png

  7. 对了,这里需要注意,我们需要给 scope 设置一个 relative 属性,来告诉我们的 containerEl 它要在谁的范围内是绝对定位。
    image.png

  8. 接下来我们进入到我们的 scope 组件内引入这个函数,调用一下看看效果。

    image.png
    2.gif
    ok,现在已经实现我们的右键弹出菜单的基本功能了。

五. 菜单位置出现的位置

  1. 在这里我们需要用到 clientX,和 clientY 这两个属性。
    image.png

  2. 如果你是第一次看到这个属性,那么我简单介绍一下。
    image.png
    假设我在屏幕的上点击了一下(类比上图的红点出),那么此时这个点到屏幕最左边的距离就是 clientX,同理到屏幕顶部的距离就是 clientY

  3. 聪明的你一定想到了,那我此时将 containerEltopleft 的值分别设置成这两个属性的值,不就恰好会让菜单出现在我们的右边吗?我们试一下。
    image.png
    然后看看效果:
    3.gif

  4. 目前看起来一切正常,但是我们需要考虑一个边界情况。
    image.png
    当我们距离屏幕右侧过近的时候,此时右键会导致有部分内容被遮挡。所以我们要想办法解决这个边界情况。

六. 解决右侧过近的问题

  1. 不要觉得很难,其实目前我们要做的事情很简单。
    image.png

  2. 如上图,我们仅仅只需要去判断
    scope 的 clientWidth 的长度 - clientX 的长度= 是否大于containerEl 的 offsetWidth ?
    如果大于,则调转 left 的方向为 right ,并设置 right=0px 即可。

  3. 如果上面所说的 offsetWidthclientWidth 你还不了解。我强烈建议你请点击这篇博文先去了解清楚这几个 width 属性到底代表着什么意思,因为对于前端开发来说,这是极其重要的几个属性。如果你之后要接触移动端,那么这是你必须掌握的知识点。
    你必须知道的 clientWdith,scrollWidth,offsetWidth

  4. 既然知道了原理,那么代码写起来就非常简单了,在此之前在这里我们需要调整一下 scope.appendChild 的执行时机。
    image.png
    我们测试一下效果。
    4.gif

七. 增强该函数的健壮性

  1. 目前这个框我们无法确保它的唯一性,所以我们还需要改造一下这个函数。

  2. 增加一个变量 isShow ,我们需要知道当前的 Menu 菜单是否正在展示。
    image.png

  3. containerElconst 声明变为 let 声明。并将创造时机延迟到调用右键时再创建,这样我们就能保证每次右键制造的这个 Menu 组件是都是全新的。(不然就会出现沿用上一次 css 属性,导致样式错乱的 bug )
    image.png

  4. 获取 scope 元素的时机也推迟到用户点击右键的时候再获取。(因为下面的 close 函数也需要用到这个变量)
    image.png

  5. 拆分两个函数,一个打开 openMenu 函数,一个关闭函数 closeMenu

    image.png
    image.png

  6. 最后在 window.oncontextmenu 的匿名函数里去调取这两个函数。
    image.png

  7. 然后我们将这三个变量暴露出去。
    image.png

八. 右键菜单的使用方法

  1. 我们进到 scope.vue 组件内,引入。
    image.png

  2. 这样我们既可以通过右键创建这个菜单栏,也可以自己在合适的时间去做一些逻辑判断手动打开。

  3. 效果如下
    5.gif

源码

  1. Menu.vue 的源码。
<script lang="ts" setup>
import { ref } from "vue"const menuItemsGroup = [{name: "查看(V)",arrow: true,action: () => {console.log("查看")},},{name: "排序方式(O)",arrow: false,action: () => {console.log("刷新")},},{name: "刷新(E)",arrow: false,action: () => {console.log("刷新")},},{name: "粘贴(P)",arrow: false,action: () => {console.log("刷新")},},{name: "粘贴快捷方式(S)",arrow: false,action: () => {console.log("刷新")},},{name: "新建(W)",arrow: false,action: () => {console.log("刷新")},},{name: "个性化(R)",arrow: false,action: () => {console.log("刷新")},},
]
</script>
<template><divclass="w-17rem bg-#ECECEC flex flex-col py-0.5rem shadow-[4px_4px_5px_2px_rgba(0,0,0,0.3)]"><divv-for="(item, i) in menuItemsGroup":key="i"@click="item.action"class="w-full h-2.5rem px-3rem text-1.5rem leading-2.5rem text-black hover:bg-white mb-0.3rem":class="[3, 5, 6].includes(i) ? `b-t-1px b-gray` : `static`"><span>{{ item.name }}</span></div></div>
</template>
  1. 这是 openContextMenus 的源码。
import { h, render } from "vue"import Menu from "./Menu.vue"export function openContextMenus() {let isShow = falselet scope: HTMLElement | null // 拿到桌面元素let containerEl: HTMLDivElement // 创建一个容器元素,给 render 先用着window.oncontextmenu = function (e: MouseEvent) {e.preventDefault()if (isShow) closeMenu()openMenu(e)}//tips: open the menufunction openMenu(e: MouseEvent) {scope = document.getElementById("PCDesktop")containerEl = document.createElement("div")const vnode = h(Menu)render(vnode, containerEl) //将 vnode 传递给 render 函数containerEl.style.position = "absolute"scope?.appendChild(containerEl) // 1. 为了拿到 offsetWidth,因为只有出现在浏览器才会产生 offsetWidth 属性值,我们需要先渲染出真实 domconst { offsetWidth } = containerEl //2 .取出 containerEl 的真实宽度const { clientWidth } = scope! //3. 获取父元素的 clientWidth 准备进行计算const { clientX, clientY } = e //4. 取出 click 时鼠标的坐标const _X = clientWidth - clientX > offsetWidth ? "left" : "right" //调整方向const _X_offset = clientWidth - clientX // 如果是需要显示在左边,则需要获取当前的差值containerEl.style.top = `${clientY}px`containerEl.style[_X] = _X === "left" ? `${clientX}px` : `${_X_offset}px`isShow = true}//tips: close the menufunction closeMenu() {if (isShow) {render(null, containerEl)scope?.removeChild(containerEl)console.log("清楚")isShow = false}}return {isShow,openMenu,closeMenu,}
}

结语

最近在实现一个 window 的全套 UI ,代码开源到了 github
image.png
我会在之后一直更新类似的内容,包括拖拽的实现。
如果你觉得本文对你有帮助,还希望点个赞

赠人玫瑰,手有余香🌹

相关文章:

Vue3 如何实现一个函数式右键菜单(ContextMenus)

前言: 最近在公司 PC 端的项目中使用到了右键出现菜单选项这样的一个工作需求&#xff0c;并且自己现在也在实现一个偶然迸发的 idea&#xff08; 想用前端实现一个 windows 系统从开机到桌面的 UI&#xff09;&#xff0c;其中也要用到右键弹出菜单这样的一个功能&#xff0c;…...

ffmpeg转码转封装小工具开发

如下图所示&#xff0c;是本人开发的一个转码转封装小工具 其中目标文件视频编码格式支持&#xff1a;H264&#xff0c;H265&#xff0c;VP8&#xff0c;VP9。 目标文件封装格式支持&#xff1a;mp4,mkv,avi,mov,flv。 目标文件音频编码格式支持两个&#xff0c;COPY和AAC&am…...

重入和线程安全

在整个文档中&#xff0c;重入和线程安全用于标记类和函数&#xff0c;从而表明怎样在多线程应用中使用它们。 线程安全函数可以从多个线程同时调用&#xff0c;即使调用使用共享数据也是如此&#xff0c;因为对共享数据的所有引用都是序列化的。也可以从多个线程同时调用重入…...

MySQL数据库06——条件查询(WHERE)

MySQL条件查询&#xff0c;主要是对数据库里面的数据按照一定条件进行筛选&#xff0c;主要依靠的是WHERE语句进行。 先来了解一下基础的条件运算。 关系运算符 逻辑运算符 逻辑运算符优先级&#xff1a;NOT>AND>OR&#xff0c;关系运算符>逻辑运算符 SQL特殊运算符…...

Lesson 6.5 机器学习调参基础理论与网格搜索

文章目录一、机器学习调参理论基础1. 机器学习调参目标及基本方法2. 基于网格搜索的超参数的调整方法2.1 参数空间2.2 交叉验证与评估指标二、基于 Scikit-Learn 的网格搜索调参1. sklearn 中网格搜索的基本说明2. sklearn 中 GridSearchCV 的参数解释3. sklearn 中 GridSearch…...

leetcode: Two Sum

leetcode: Two Sum1. 题目1.1 题目描述2. 解答2.1 baseline2.2 基于baseline的思考2.3 优化思路的实施2.3.1 C中的hashmap2.3.2 实施2.3.3 再思考2.3.4 最终实施3. 总结1. 题目 1.1 题目描述 Given an array of integers nums and an integer target, return indices of the …...

共享模型之无锁(三)

1.原子累加器 示例代码: public class TestAtomicAdder {public static void main(String[] args) {for (int i 0; i < 5; i) {demo(() -> new AtomicLong(0),(adder) -> adder.getAndIncrement());}for (int i 0; i < 5; i) {demo(() -> new LongAdder(),(…...

微信小程序 Springboot校运会高校运动会管理系统

3.1小程序端 小程序登录页面&#xff0c;用户也可以在此页面进行注册并且登录等。 登录成功后可以在我的个人中心查看自己的个人信息或者修改信息等 在广播信息中我们可以查看校运会发布的一些信息情况。 在首页我们可以看到校运会具体有什么项目运动。 在查看具体有什么活动我…...

走进独自开,带你轻松干副业

今天给大家分享一个开发者的福利平台——独自开&#xff08;点击直接注册&#xff09;&#xff0c;让你在家就能解决收入问题。 文章目录一、平台介绍二、系统案例三、获取收益四、使用平台1、用户注册2、用户认证3、任务报价五、文末总结一、平台介绍 简单说明 独自开信息科技…...

SpringBoot+Vue实现师生健康信息管理系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7/8.0 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.3.9 浏…...

数据库第四章节第三次作业内容

1、显示所有职工的基本信息。 2、查询所有职工所属部门的部门号&#xff0c;不显示重复的部门号。 3、求出所有职工的人数。 4、列出最高工和最低工资。 5、列出职工的平均工资和总工资。 6、创建一个只有职工号、姓名和参加工作的新表&#xff0c;名为工作日期表…...

一篇五分生信临床模型预测文章代码复现——FIgure 9.列线图构建,ROC分析,DCA分析 (四)

之前讲过临床模型预测的专栏,但那只是基础版本,下面我们以自噬相关基因为例子,模仿一篇五分文章,将图和代码复现出来,学会本专栏课程,可以具备发一篇五分左右文章的水平: 本专栏目录如下: Figure 1:差异表达基因及预后基因筛选(图片仅供参考) Figure 2. 生存分析,…...

神经网络实战--使用迁移学习完成猫狗分类

前言&#xff1a; Hello大家好&#xff0c;我是Dream。 今天来学习一下如何使用基于tensorflow和keras的迁移学习完成猫狗分类&#xff0c;欢迎大家一起前来探讨学习~ 本文目录&#xff1a;一、加载数据集1.调用库函数2.加载数据集3.数据集管理二、猫狗数据集介绍1.猫狗数据集介…...

Attention机制 学习笔记

学习自https://easyai.tech/ai-definition/attention/ Attention本质 Attention&#xff08;注意力&#xff09;机制如果浅层的理解&#xff0c;跟他的名字非常匹配。他的核心逻辑就是“从关注全部到关注重点”。 比如我们人在看图片时&#xff0c;对图片的不同地方的注意力…...

数据类型与运算符

1.字符型作用: 字符型变量用于显示单个字符语法: char cc a ;注意1: 在显示字符型变量时&#xff0c;用单引号将字符括起来,不要用双引号注意2: 单引号内只能有一个字符&#xff0c;不可以是字符串C和C中字符型变量只占用1个字节。字符型变是并不是把字符本身放到内存中存储&am…...

算法刷题-二叉树的锯齿形层序遍历、用栈实现队列 栈设计、买卖股票的最佳时机 IV

文章目录二叉树的锯齿形层序遍历&#xff08;树、广度优先搜索&#xff09;用栈实现队列&#xff08;栈、设计&#xff09;买卖股票的最佳时机 IV&#xff08;数组、动态规划&#xff09;二叉树的锯齿形层序遍历&#xff08;树、广度优先搜索&#xff09; 给定一个二叉树&…...

华为OD机试 - 最小传递延迟(Python)| 代码编写思路+核心知识点

最小传递延迟 题目 通讯网络中有 N 个网络节点 用 1 ~ N 进行标识 网络通过一个有向无环图进行表示 其中图的边的值,表示节点之间的消息传递延迟 现给定相连节点之间的延时列表 times[i]={u,v,w} 其中 u 表示源节点,v 表示目的节点,w 表示 u 和 v 之间的消息传递延时 请计…...

集中供热调度系统天然气仪表内网仪表图像识别案例

一、项目需求 出于能耗采集与冬季集中供暖工作的节能和能耗分析需要&#xff0c;要采集现场的6块天然气表计&#xff0c;并存储进入客户的mySQL数据库中&#xff0c;现场采集的表计不允许接线&#xff0c;且网络环境为内网环境&#xff0c;需要采集表计数据并存入数据库&#…...

笔试题-2023-复旦微-数字IC设计【纯净题目版】

回到首页:2023 数字IC设计秋招复盘——数十家公司笔试题、面试实录 推荐内容:数字IC设计学习比较实用的资料推荐 题目背景 笔试时间:2022.07.26应聘岗位:数字前端工程师笔试时长:120min笔试平台:赛码题目类型:基础题(10道)、选做题(10道)、验证题(5道)主观评价 难…...

【Linux】冯诺依曼体系结构和操作系统概念

文章目录&#x1f3aa; 冯诺依曼体系结构&#x1f680;1.体系概述&#x1f680;2.CPU和内存的数据交换&#x1f680;3.体系结构中数据的流动&#x1f3aa; 操作系统概念理解&#x1f680;1.简述&#x1f680;2.设计目的&#x1f680;3.定位&#x1f680;4.理解&#x1f680;5.管…...

为什么你的unipush消息收不到?详解个推通道状态检测与事件触发逻辑

为什么你的UniPush消息收不到&#xff1f;深度解析推送失效的7大关键因素 在移动应用开发中&#xff0c;消息推送是维系用户活跃度的核心功能之一。许多开发者在使用UniPush服务时&#xff0c;经常会遇到消息未能如期送达的困扰。本文将系统性地剖析消息推送失效的底层逻辑&…...

新手避坑指南:用DJI NAZA-LITE飞控组装F450无人机,从焊接电调到GPS校准的完整流程

新手避坑指南&#xff1a;用DJI NAZA-LITE飞控组装F450无人机&#xff0c;从焊接电调到GPS校准的完整流程 第一次组装无人机就像玩一场高风险的拼图游戏——每个零件的位置、每根接线的顺序都可能影响最终能否安全起飞。作为过来人&#xff0c;我清楚地记得焊接电调时锡珠飞溅的…...

Python 字典遍历全攻略:5 种常用方法 + 性能对比 + 实战优化技巧

在 Python 开发中&#xff0c;字典&#xff08;dict&#xff09; 是最常用的数据结构之一&#xff0c;以键值对形式存储数据&#xff0c;具备查询快、易操作的特点。而字典的遍历是日常开发中高频操作 —— 从简单的数据读取&#xff0c;到大规模数据处理、接口返回值解析&…...

大致说一下spring bean的生命周期

面试 1、实例化 Bean 2、给 Bean 属性赋值 3、初始化 Bean 4、使用 Bean 5、销毁 Bean package com.example.demo.bean;import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import org.springframework.beans.factory.annotation.Value; import …...

Ant Design生态系统全解析:从React到Vue、Angular和Blazor

Ant Design生态系统全解析&#xff1a;从React到Vue、Angular和Blazor 【免费下载链接】awesome-ant-design A curated list of Ant Design resources and related projects. The main idea is that everyone can contribute here, so we can have a central repository of inf…...

如何快速恢复丢失的Ren‘Py游戏源码:Unrpyc终极反编译指南

如何快速恢复丢失的RenPy游戏源码&#xff1a;Unrpyc终极反编译指南 【免费下载链接】unrpyc A renpy script decompiler 项目地址: https://gitcode.com/gh_mirrors/un/unrpyc 你是否曾经遇到过精心制作的RenPy游戏源代码意外丢失&#xff0c;只剩下编译后的.rpyc文件&…...

如何突破数据标注瓶颈?Label Studio全攻略:从多模态标注到AI协作

如何突破数据标注瓶颈&#xff1f;Label Studio全攻略&#xff1a;从多模态标注到AI协作 【免费下载链接】label-studio Label Studio is a multi-type data labeling and annotation tool with standardized output format 项目地址: https://gitcode.com/GitHub_Trending/l…...

从理论到实践:在快马平台构建基于openclaw的物流分拣仿真系统

最近在研究物流自动化分拣系统时&#xff0c;发现openclaw机械爪控制在实际应用中存在不少痛点。传统开发流程需要从零搭建仿真环境、编写控制逻辑、调试物理交互&#xff0c;整个过程耗时耗力。于是尝试用InsCode(快马)平台快速构建了一个物流分拣仿真系统&#xff0c;效果出乎…...

OpenClaw安全防护指南:百川2-13B-4bits量化模型权限管控实践

OpenClaw安全防护指南&#xff1a;百川2-13B-4bits量化模型权限管控实践 1. 为什么需要安全防护&#xff1f; 当我第一次把OpenClaw接入百川2-13B-4bits量化模型时&#xff0c;那种兴奋感至今难忘——终于可以在本地运行一个强大的AI助手了。但很快&#xff0c;一个意外让我意…...

为什么92%的候选人栽在FastAPI流式响应题上?——基于137份大厂AI后端面试记录的深度复盘

第一章&#xff1a;FastAPI 2.0流式响应的核心机制与演进脉络FastAPI 2.0 对流式响应&#xff08;Streaming Response&#xff09;进行了底层重构&#xff0c;将原先依赖 Starlette 的 StreamingResponse 封装升级为原生异步生成器驱动模型&#xff0c;并深度整合 ASGI 3.0 规范…...