【vue】 如何在划词选择的文本上添加右键菜单
·
基础实现步骤
1. 创建右键菜单组件
首先创建一个独立的右键菜单组件 ContextMenu.vue:
<template>
<div
v-show="visible"
class="context-menu"
:style="{ left: `${position.x}px`, top: `${position.y}px` }"
@click.stop
>
<div class="menu-item" @click="handleCopy">
<span class="icon">📋</span> 复制
</div>
<div class="menu-item" @click="handleHighlight">
<span class="icon">🖍️</span> 高亮
</div>
<div class="menu-item" @click="handleSearch">
<span class="icon">🔍</span> 搜索
</div>
<div class="menu-divider"></div>
<div class="menu-item" @click="handleShare">
<span class="icon">↗️</span> 分享
</div>
</div>
</template>
<script>
export default {
data() {
return {
visible: false,
position: { x: 0, y: 0 },
selectedText: '',
selectedRange: null
}
},
methods: {
showMenu(e, selectedText, range) {
this.selectedText = selectedText
this.selectedRange = range
this.position = { x: e.clientX, y: e.clientY }
this.visible = true
// 阻止默认右键菜单
e.preventDefault()
},
hideMenu() {
this.visible = false
},
handleCopy() {
navigator.clipboard.writeText(this.selectedText)
.then(() => {
this.$toast.success('已复制到剪贴板')
})
.catch(err => {
console.error('复制失败:', err)
})
this.hideMenu()
},
handleHighlight() {
if (this.selectedRange) {
const span = document.createElement('span')
span.className = 'text-highlight'
span.style.backgroundColor = 'rgba(255, 255, 0, 0.5)'
this.selectedRange.surroundContents(span)
}
this.hideMenu()
},
handleSearch() {
const searchUrl = `https://www.google.com/search?q=${encodeURIComponent(this.selectedText)}`
window.open(searchUrl, '_blank')
this.hideMenu()
},
handleShare() {
if (navigator.share) {
navigator.share({
title: '分享选中文本',
text: this.selectedText
}).catch(err => {
console.log('分享取消:', err)
})
} else {
this.handleCopy()
}
this.hideMenu()
}
},
mounted() {
document.addEventListener('click', this.hideMenu)
document.addEventListener('contextmenu', this.hideMenu)
},
beforeDestroy() {
document.removeEventListener('click', this.hideMenu)
document.removeEventListener('contextmenu', this.hideMenu)
}
}
</script>
<style scoped>
.context-menu {
position: fixed;
background: white;
border: 1px solid #e1e1e1;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
z-index: 9999;
min-width: 180px;
padding: 5px 0;
font-size: 14px;
}
.menu-item {
padding: 8px 15px;
cursor: pointer;
display: flex;
align-items: center;
}
.menu-item:hover {
background-color: #f5f5f5;
}
.menu-item .icon {
margin-right: 8px;
font-size: 16px;
}
.menu-divider {
height: 1px;
background-color: #e1e1e1;
margin: 5px 0;
}
.text-highlight {
padding: 0 2px;
border-radius: 2px;
}
</style>
2. 在父组件中使用
<template>
<div class="text-container">
<!-- 注册右键菜单组件 -->
<context-menu ref="contextMenu" />
<!-- 可选中文本的内容区域 -->
<div
class="selectable-content"
@mouseup="handleMouseUp"
@contextmenu="handleContextMenu"
>
<h2>示例文本内容</h2>
<p>这里是可选择的文本段落。你可以用鼠标划选任意文本,然后右键点击会出现自定义菜单。</p>
<p>菜单提供了复制、高亮、搜索和分享等功能,增强了用户体验。</p>
<p>Vue.js 是一个渐进式 JavaScript 框架,用于构建用户界面。</p>
</div>
</div>
</template>
<script>
import ContextMenu from './ContextMenu.vue'
export default {
components: { ContextMenu },
methods: {
handleMouseUp() {
// 延迟处理以确保选中文本已经更新
setTimeout(() => {
const selection = window.getSelection()
const selectedText = selection.toString().trim()
if (selectedText.length > 0) {
// 存储当前选中范围
this.currentSelection = selection
this.currentRange = selection.getRangeAt(0)
}
}, 10)
},
handleContextMenu(e) {
const selection = window.getSelection()
const selectedText = selection.toString().trim()
if (selectedText.length > 0) {
// 显示自定义菜单
this.$refs.contextMenu.showMenu(
e,
selectedText,
selection.getRangeAt(0)
}
}
}
}
</script>
<style>
.selectable-content {
user-select: text;
line-height: 1.6;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.selectable-content p {
margin-bottom: 15px;
}
.text-highlight {
background-color: rgba(255, 255, 0, 0.5);
padding: 0 2px;
border-radius: 2px;
}
</style>
高级功能扩展
1. 添加动画效果
在右键菜单组件中添加过渡动画:
<template>
<transition
name="menu"
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
>
<div
v-show="visible"
class="context-menu"
:style="{ left: `${position.x}px`, top: `${position.y}px` }"
@click.stop
>
<!-- 菜单内容 -->
</div>
</transition>
</template>
<script>
export default {
methods: {
beforeEnter(el) {
el.style.opacity = 0
el.style.transform = 'scale(0.95) translateY(-5px)'
},
enter(el, done) {
const duration = 150
const startTime = Date.now()
const animate = () => {
const elapsed = Date.now() - startTime
const progress = Math.min(elapsed / duration, 1)
el.style.opacity = progress
el.style.transform = `scale(${0.95 + 0.05 * progress}) translateY(${-5 + 5 * progress}px)`
if (progress < 1) {
requestAnimationFrame(animate)
} else {
done()
}
}
requestAnimationFrame(animate)
},
leave(el, done) {
const duration = 100
const startTime = Date.now()
const animate = () => {
const elapsed = Date.now() - startTime
const progress = Math.min(elapsed / duration, 1)
el.style.opacity = 1 - progress
el.style.transform = `scale(${1 - 0.05 * progress}) translateY(${5 * progress}px)`
if (progress < 1) {
requestAnimationFrame(animate)
} else {
done()
}
}
requestAnimationFrame(animate)
}
}
}
</script>
2. 支持动态菜单项
<script>
// 在ContextMenu组件中
export default {
props: {
menuItems: {
type: Array,
default: () => [
{
id: 'copy',
label: '复制',
icon: '📋',
action: 'handleCopy',
disabled: false
},
{
id: 'highlight',
label: '高亮',
icon: '🖍️',
action: 'handleHighlight'
},
{
id: 'search',
label: '搜索',
icon: '🔍',
action: 'handleSearch'
},
{
type: 'divider'
},
{
id: 'share',
label: '分享',
icon: '↗️',
action: 'handleShare',
disabled: !navigator.share
}
]
}
},
computed: {
processedMenuItems() {
return this.menuItems.filter(item => {
// 过滤掉禁用项
return !item.disabled
})
}
}
}
</script>
<template>
<!-- 动态渲染菜单项 -->
<div v-for="(item, index) in processedMenuItems" :key="index">
<div
v-if="item.type === 'divider'"
class="menu-divider"
></div>
<div
v-else
class="menu-item"
@click="executeAction(item)"
>
<span class="icon">{{ item.icon }}</span> {{ item.label }}
</div>
</div>
</template>
3. 创建自定义指令
为了更方便地在多个地方使用,可以创建一个自定义指令:
// directives/selectionContextMenu.js
export default {
bind(el, binding, vnode) {
const contextMenu = vnode.context.$refs[binding.arg]
let currentRange = null
el.addEventListener('mouseup', () => {
setTimeout(() => {
const selection = window.getSelection()
const selectedText = selection.toString().trim()
if (selectedText.length > 0) {
currentRange = selection.getRangeAt(0)
}
}, 10)
})
el.addEventListener('contextmenu', (e) => {
const selection = window.getSelection()
const selectedText = selection.toString().trim()
if (selectedText.length > 0 && contextMenu) {
contextMenu.showMenu(e, selectedText, currentRange)
e.preventDefault()
}
})
}
}
// 在main.js中注册
import SelectionContextMenu from './directives/selectionContextMenu'
Vue.directive('selection-menu', SelectionContextMenu)
// 使用方式
<div v-selection-menu:contextMenu>
<!-- 可选中文本 -->
</div>
注意事项和最佳实践
-
浏览器兼容性:
window.getSelection()和RangeAPI 在现代浏览器中广泛支持clipboard.writeText()需要HTTPS环境或localhostnavigator.share()只在部分移动浏览器中可用
-
性能优化:
- 避免在大型文档上频繁操作DOM
- 使用事件委托如果有多个可选中区域
- 考虑节流鼠标事件处理
-
无障碍访问:
- 为菜单添加适当的ARIA属性
- 确保可以通过键盘操作
- 提供视觉反馈
-
与UI框架集成:
- 可以包装Element UI/Vuetify等的菜单组件
- 保持一致的视觉风格
各位大佬,点点赞呗
更多推荐


所有评论(0)