基础实现步骤

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>

注意事项和最佳实践

  1. 浏览器兼容性

    • window.getSelection()Range API 在现代浏览器中广泛支持
    • clipboard.writeText() 需要HTTPS环境或localhost
    • navigator.share() 只在部分移动浏览器中可用
  2. 性能优化

    • 避免在大型文档上频繁操作DOM
    • 使用事件委托如果有多个可选中区域
    • 考虑节流鼠标事件处理
  3. 无障碍访问

    • 为菜单添加适当的ARIA属性
    • 确保可以通过键盘操作
    • 提供视觉反馈
  4. 与UI框架集成

    • 可以包装Element UI/Vuetify等的菜单组件
    • 保持一致的视觉风格

各位大佬,点点赞呗

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐