背景说明
在现代Web应用中,划词选择和上下文菜单是提升用户体验的重要功能。传统的浏览器提供了基础的文本选择和默认右键菜单,但往往无法满足特定业务场景的需求:
- 功能局限性:浏览器默认的右键菜单无法添加自定义功能选项(如"翻译"、"收藏"等)
- 视觉一致性:默认菜单样式与网站设计风格不匹配,影响整体UI体验
- 业务需求:许多应用需要根据选中内容动态显示不同的操作选项
功能说明
-
划词选择:鼠标拖动选择文本时会临时高亮显示选中的内容
-
右键菜单:选中文本后右键点击会显示自定义菜单(复制、搜索、高亮)
-
菜单功能
:
- 复制:将选中文本复制到剪贴板
- 搜索:使用百度搜索选中文本
- 翻译:翻译显示选中的文本
解析步骤
- 监听鼠标抬起事件获取选中文本
- 阻止默认右键菜单
- 显示自定义右键菜单
- 根据选中文本执行菜单操作
父组件引用
<template>
<div>
<text-selection-context-menu>
<!-- 这里放你的内容 -->
<p>这是一段可以划词选择并显示右键菜单的文本...</p>
<p>另一段可以选择的文本...</p>
</text-selection-context-menu>
</div>
</template>
<script>
import TextSelectionContextMenu from "./Menu.vue";
export default {
components: {
TextSelectionContextMenu,
},
};
</script>
菜单复制组件
<template>
<div @mouseup="handleMouseUp" @contextmenu.prevent>
<!-- 你的内容区域 -->
<slot></slot>
<!-- 右键菜单 -->
<div
v-if="showMenu"
class="custom-context-menu"
:style="{ left: menuPosition.x + 'px', top: menuPosition.y + 'px' }"
v-click-outside="closeMenu"
>
<ul>
<li @click="handleMenuAction('search')">搜索</li>
<li @click="handleMenuAction('translate')">翻译</li>
<li @click="handleMenuAction('copy')">复制</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: "TextSelectionContextMenu",
data() {
return {
showMenu: false,
selectedText: "",
menuPosition: {
x: 0,
y: 0,
},
};
},
directives: {
// 点击菜单外部关闭菜单的指令
"click-outside": {
bind(el, binding, vnode) {
el.clickOutsideEvent = function (event) {
if (!(el === event.target || el.contains(event.target))) {
vnode.context[binding.expression](event);
}
};
document.body.addEventListener("click", el.clickOutsideEvent);
},
unbind(el) {
document.body.removeEventListener("click", el.clickOutsideEvent);
},
},
},
methods: {
handleMouseUp(event) {
// 检查是否是右键点击
if (event.button !== 2) return;
const selection = window.getSelection();
const text = selection.toString().trim();
if (text.length > 0) {
this.selectedText = text;
this.menuPosition = {
x: event.clientX,
y: event.clientY,
};
this.showMenu = true;
} else {
this.closeMenu();
}
},
handleMenuAction(action) {
switch (action) {
case "search":
window.open(
`https://www.google.com/search?q=${encodeURIComponent(
this.selectedText
)}`
);
break;
case "translate":
window.open(
`https://translate.google.com/?text=${encodeURIComponent(
this.selectedText
)}`
);
break;
case "copy":
navigator.clipboard
.writeText(this.selectedText)
.then(() => {
this.$message.success("已复制到剪贴板");
})
.catch((err) => {
console.error("复制失败:", err);
this.$message.error("复制失败");
});
break;
default:
console.log("未知操作:", action);
}
this.closeMenu();
},
closeMenu() {
this.showMenu = false;
this.selectedText = "";
},
},
};
</script>
<style scoped>
.custom-context-menu {
position: fixed;
background: white;
border: 1px solid #ddd;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 1000;
}
.custom-context-menu ul {
list-style: none;
margin: 0;
padding: 0;
}
.custom-context-menu li {
padding: 8px 15px;
cursor: pointer;
}
.custom-context-menu li:hover {
background-color: #f0f0f0;
}
</style>