Skip to content

事件处理

在 Vue 中,我们可以使用 v-on 指令(简写为 @)来监听 DOM 事件,并在事件触发时执行 JavaScript 代码。

监听事件

内联事件处理器

事件处理器的值可以是:

  1. 内联事件处理器:事件被触发时执行的内联 JavaScript 语句(与 onclick 类似)
vue
<template>
  <div>
    <h2>内联事件处理器</h2>
    <p>计数器: {{ count }}</p>
    <button @click="count++">增加 1</button>
    <button @click="count--">减少 1</button>
    <button @click="count = 0">重置</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

方法事件处理器

  1. 方法事件处理器:一个指向组件上定义的方法的属性名或是路径
vue
<template>
  <div>
    <h2>方法事件处理器</h2>
    <p>计数器: {{ count }}</p>
    <button @click="increment">增加</button>
    <button @click="decrement">减少</button>
    <button @click="reset">重置</button>
    
    <div class="demo-section">
      <h3>问候演示</h3>
      <input v-model="name" placeholder="输入你的名字">
      <button @click="greet">问候</button>
      <p v-if="greeting">{{ greeting }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)
const name = ref('')
const greeting = ref('')

function increment() {
  count.value++
}

function decrement() {
  count.value--
}

function reset() {
  count.value = 0
}

function greet() {
  if (name.value) {
    greeting.value = `你好,${name.value}!`
  } else {
    greeting.value = '请先输入你的名字'
  }
}
</script>

<style scoped>
.demo-section {
  margin-top: 30px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
}

.demo-section input {
  padding: 8px;
  margin-right: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.demo-section button {
  padding: 8px 16px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

在内联处理器中调用方法

除了直接绑定方法名,你还可以在内联事件处理器中调用方法。这允许我们向方法传入自定义参数而不是原生事件:

vue
<template>
  <div class="inline-methods-demo">
    <h2>在内联处理器中调用方法</h2>
    
    <div class="calculator">
      <h3>简单计算器</h3>
      <p>当前值: {{ result }}</p>
      
      <div class="button-grid">
        <button @click="calculate('add', 1)">+1</button>
        <button @click="calculate('add', 5)">+5</button>
        <button @click="calculate('add', 10)">+10</button>
        <button @click="calculate('subtract', 1)">-1</button>
        <button @click="calculate('subtract', 5)">-5</button>
        <button @click="calculate('subtract', 10)">-10</button>
        <button @click="calculate('multiply', 2)">×2</button>
        <button @click="calculate('divide', 2)">÷2</button>
        <button @click="calculate('reset')">重置</button>
      </div>
    </div>
    
    <div class="color-picker">
      <h3>颜色选择器</h3>
      <p>当前颜色: <span :style="{ color: currentColor }">{{ currentColor }}</span></p>
      
      <div class="color-buttons">
        <button 
          v-for="color in colors" 
          :key="color.name"
          @click="changeColor(color.value)"
          :style="{ backgroundColor: color.value }"
          class="color-btn"
        >
          {{ color.name }}
        </button>
      </div>
    </div>
    
    <div class="message-system">
      <h3>消息系统</h3>
      <div class="message-buttons">
        <button @click="showMessage('success', '操作成功!')">成功消息</button>
        <button @click="showMessage('warning', '请注意!')">警告消息</button>
        <button @click="showMessage('error', '发生错误!')">错误消息</button>
        <button @click="showMessage('info', '这是一条信息')">信息消息</button>
      </div>
      
      <div v-if="message" class="message" :class="message.type">
        <strong>{{ message.type.toUpperCase() }}:</strong> {{ message.text }}
        <button @click="clearMessage" class="close-btn">×</button>
      </div>
    </div>
    
    <div class="todo-quick-add">
      <h3>快速添加待办</h3>
      <div class="quick-buttons">
        <button @click="addTodo('学习 Vue 3')">学习 Vue 3</button>
        <button @click="addTodo('完成项目')">完成项目</button>
        <button @click="addTodo('写文档')">写文档</button>
        <button @click="addTodo('代码审查')">代码审查</button>
      </div>
      
      <ul class="todo-list">
        <li v-for="(todo, index) in todos" :key="index" class="todo-item">
          {{ todo }}
          <button @click="removeTodo(index)" class="remove-btn">删除</button>
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const result = ref(0)
const currentColor = ref('#000000')
const message = ref(null)
const todos = ref([])

const colors = [
  { name: '红色', value: '#ff0000' },
  { name: '绿色', value: '#00ff00' },
  { name: '蓝色', value: '#0000ff' },
  { name: '黄色', value: '#ffff00' },
  { name: '紫色', value: '#800080' },
  { name: '橙色', value: '#ffa500' }
]

function calculate(operation, value = 0) {
  switch (operation) {
    case 'add':
      result.value += value
      break
    case 'subtract':
      result.value -= value
      break
    case 'multiply':
      result.value *= value
      break
    case 'divide':
      if (value !== 0) {
        result.value /= value
      }
      break
    case 'reset':
      result.value = 0
      break
  }
}

function changeColor(color) {
  currentColor.value = color
}

function showMessage(type, text) {
  message.value = { type, text }
  
  // 3秒后自动清除消息
  setTimeout(() => {
    if (message.value && message.value.type === type && message.value.text === text) {
      message.value = null
    }
  }, 3000)
}

function clearMessage() {
  message.value = null
}

function addTodo(text) {
  todos.value.push(text)
}

function removeTodo(index) {
  todos.value.splice(index, 1)
}
</script>

<style scoped>
.inline-methods-demo {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.calculator,
.color-picker,
.message-system,
.todo-quick-add {
  margin-bottom: 30px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.calculator h3,
.color-picker h3,
.message-system h3,
.todo-quick-add h3 {
  margin: 0 0 15px 0;
  color: #333;
}

.button-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  max-width: 300px;
}

.button-grid button {
  padding: 10px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-weight: bold;
  transition: all 0.3s;
  background-color: #007bff;
  color: white;
}

.button-grid button:hover {
  background-color: #0056b3;
  transform: translateY(-1px);
}

.color-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.color-btn {
  padding: 10px 15px;
  border: 2px solid #333;
  border-radius: 6px;
  cursor: pointer;
  font-weight: bold;
  color: white;
  text-shadow: 1px 1px 1px rgba(0,0,0,0.5);
  transition: all 0.3s;
}

.color-btn:hover {
  transform: scale(1.05);
  box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}

.message-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 15px;
}

.message-buttons button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-weight: bold;
  color: white;
  transition: all 0.3s;
}

.message-buttons button:nth-child(1) {
  background-color: #28a745;
}

.message-buttons button:nth-child(2) {
  background-color: #ffc107;
  color: #212529;
}

.message-buttons button:nth-child(3) {
  background-color: #dc3545;
}

.message-buttons button:nth-child(4) {
  background-color: #17a2b8;
}

.message-buttons button:hover {
  opacity: 0.8;
  transform: translateY(-1px);
}

.message {
  padding: 12px 16px;
  border-radius: 6px;
  position: relative;
  animation: slideIn 0.3s ease-out;
}

@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.message.success {
  background-color: #d4edda;
  border: 1px solid #c3e6cb;
  color: #155724;
}

.message.warning {
  background-color: #fff3cd;
  border: 1px solid #ffeaa7;
  color: #856404;
}

.message.error {
  background-color: #f8d7da;
  border: 1px solid #f5c6cb;
  color: #721c24;
}

.message.info {
  background-color: #d1ecf1;
  border: 1px solid #bee5eb;
  color: #0c5460;
}

.close-btn {
  position: absolute;
  top: 8px;
  right: 12px;
  background: none;
  border: none;
  font-size: 18px;
  cursor: pointer;
  color: inherit;
  opacity: 0.7;
}

.close-btn:hover {
  opacity: 1;
}

.quick-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 15px;
}

.quick-buttons button {
  padding: 8px 16px;
  background-color: #6c757d;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s;
}

.quick-buttons button:hover {
  background-color: #5a6268;
  transform: translateY(-1px);
}

.todo-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.todo-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px;
  margin-bottom: 5px;
  background-color: white;
  border: 1px solid #dee2e6;
  border-radius: 4px;
}

.remove-btn {
  background-color: #dc3545;
  color: white;
  border: none;
  padding: 4px 8px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}

.remove-btn:hover {
  background-color: #c82333;
}

@media (max-width: 768px) {
  .button-grid {
    grid-template-columns: repeat(2, 1fr);
  }
  
  .color-buttons,
  .message-buttons,
  .quick-buttons {
    flex-direction: column;
  }
  
  .todo-item {
    flex-direction: column;
    gap: 10px;
    text-align: center;
  }
}
</style>

访问事件参数

有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event 变量,或者使用内联箭头函数:

vue
<template>
  <div class="event-access-demo">
    <h2>访问事件参数</h2>
    
    <div class="demo-section">
      <h3>使用 $event 变量</h3>
      <button @click="handleClick('按钮被点击', $event)">
        点击我 (使用 $event)
      </button>
    </div>
    
    <div class="demo-section">
      <h3>使用内联箭头函数</h3>
      <button @click="(event) => handleClick('箭头函数点击', event)">
        点击我 (使用箭头函数)
      </button>
    </div>
    
    <div class="demo-section">
      <h3>鼠标事件信息</h3>
      <div 
        @mousemove="handleMouseMove"
        @click="handleMouseClick"
        class="mouse-area"
      >
        在这里移动鼠标或点击
        <div class="mouse-info">
          <p>鼠标位置: ({{ mouseX }}, {{ mouseY }})</p>
          <p>相对位置: ({{ relativeX }}, {{ relativeY }})</p>
          <p>最后点击: {{ lastClick }}</p>
        </div>
      </div>
    </div>
    
    <div class="demo-section">
      <h3>键盘事件</h3>
      <input 
        @keydown="handleKeyDown"
        @keyup="handleKeyUp"
        placeholder="在这里输入并观察键盘事件"
        class="keyboard-input"
      >
      <div class="keyboard-info">
        <p>按下的键: {{ pressedKey }}</p>
        <p>键码: {{ keyCode }}</p>
        <p>修饰键: {{ modifiers }}</p>
      </div>
    </div>
    
    <div class="demo-section">
      <h3>表单事件</h3>
      <form @submit="handleSubmit">
        <input 
          v-model="formData.name" 
          @input="handleInput"
          @focus="handleFocus"
          @blur="handleBlur"
          placeholder="姓名"
          class="form-input"
        >
        <input 
          v-model="formData.email" 
          @change="handleChange"
          placeholder="邮箱"
          type="email"
          class="form-input"
        >
        <button type="submit">提交</button>
      </form>
      
      <div class="form-info">
        <p>输入事件: {{ inputEvent }}</p>
        <p>焦点事件: {{ focusEvent }}</p>
        <p>变化事件: {{ changeEvent }}</p>
      </div>
    </div>
    
    <div class="event-log">
      <h3>事件日志</h3>
      <button @click="clearLog" class="clear-btn">清空日志</button>
      <div class="log-container">
        <div 
          v-for="(log, index) in eventLogs" 
          :key="index"
          class="log-entry"
        >
          <span class="log-time">{{ log.time }}</span>
          <span class="log-type">{{ log.type }}</span>
          <span class="log-details">{{ log.details }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue'

const mouseX = ref(0)
const mouseY = ref(0)
const relativeX = ref(0)
const relativeY = ref(0)
const lastClick = ref('')
const pressedKey = ref('')
const keyCode = ref('')
const modifiers = ref('')
const inputEvent = ref('')
const focusEvent = ref('')
const changeEvent = ref('')
const eventLogs = ref([])

const formData = reactive({
  name: '',
  email: ''
})

function addLog(type, details) {
  eventLogs.value.unshift({
    time: new Date().toLocaleTimeString(),
    type,
    details
  })
  
  // 保持日志数量在合理范围内
  if (eventLogs.value.length > 50) {
    eventLogs.value.pop()
  }
}

function handleClick(message, event) {
  console.log('点击事件:', message, event)
  addLog('Click', `${message} - 按钮: ${event.button}, 坐标: (${event.clientX}, ${event.clientY})`)
}

function handleMouseMove(event) {
  mouseX.value = event.clientX
  mouseY.value = event.clientY
  relativeX.value = event.offsetX
  relativeY.value = event.offsetY
}

function handleMouseClick(event) {
  lastClick.value = `(${event.offsetX}, ${event.offsetY}) at ${new Date().toLocaleTimeString()}`
  addLog('Mouse Click', `位置: (${event.offsetX}, ${event.offsetY}), 按钮: ${event.button}`)
}

function handleKeyDown(event) {
  pressedKey.value = event.key
  keyCode.value = event.code
  
  const mods = []
  if (event.ctrlKey) mods.push('Ctrl')
  if (event.shiftKey) mods.push('Shift')
  if (event.altKey) mods.push('Alt')
  if (event.metaKey) mods.push('Meta')
  modifiers.value = mods.join(' + ') || '无'
  
  addLog('Key Down', `键: ${event.key}, 代码: ${event.code}, 修饰键: ${modifiers.value}`)
}

function handleKeyUp(event) {
  addLog('Key Up', `键: ${event.key}`)
}

function handleInput(event) {
  inputEvent.value = `输入: "${event.target.value}" at ${new Date().toLocaleTimeString()}`
  addLog('Input', `值: "${event.target.value}"`)
}

function handleFocus(event) {
  focusEvent.value = `获得焦点 at ${new Date().toLocaleTimeString()}`
  addLog('Focus', `元素获得焦点: ${event.target.placeholder}`)
}

function handleBlur(event) {
  focusEvent.value = `失去焦点 at ${new Date().toLocaleTimeString()}`
  addLog('Blur', `元素失去焦点: ${event.target.placeholder}`)
}

function handleChange(event) {
  changeEvent.value = `值改变: "${event.target.value}" at ${new Date().toLocaleTimeString()}`
  addLog('Change', `新值: "${event.target.value}"`)
}

function handleSubmit(event) {
  event.preventDefault()
  addLog('Form Submit', `表单提交 - 姓名: "${formData.name}", 邮箱: "${formData.email}"`)
  alert(`表单提交:\n姓名: ${formData.name}\n邮箱: ${formData.email}`)
}

function clearLog() {
  eventLogs.value = []
  addLog('System', '日志已清空')
}
</script>

<style scoped>
.event-access-demo {
  max-width: 900px;
  margin: 0 auto;
  padding: 20px;
}

.demo-section {
  margin-bottom: 25px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.demo-section h3 {
  margin: 0 0 15px 0;
  color: #333;
}

.demo-section button {
  padding: 10px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 16px;
  transition: all 0.3s;
}

.demo-section button:hover {
  background-color: #0056b3;
  transform: translateY(-1px);
}

.mouse-area {
  width: 100%;
  height: 200px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: white;
  font-size: 18px;
  cursor: crosshair;
  user-select: none;
}

.mouse-info {
  margin-top: 15px;
  font-size: 14px;
  text-align: center;
}

.mouse-info p {
  margin: 5px 0;
}

.keyboard-input {
  width: 100%;
  padding: 12px;
  border: 2px solid #ced4da;
  border-radius: 6px;
  font-size: 16px;
  transition: border-color 0.3s;
}

.keyboard-input:focus {
  outline: none;
  border-color: #007bff;
}

.keyboard-info {
  margin-top: 15px;
  padding: 15px;
  background-color: white;
  border-radius: 6px;
  border: 1px solid #dee2e6;
}

.keyboard-info p {
  margin: 8px 0;
  font-family: monospace;
}

.form-input {
  width: 100%;
  padding: 10px;
  margin-bottom: 10px;
  border: 1px solid #ced4da;
  border-radius: 4px;
  font-size: 16px;
  box-sizing: border-box;
}

.form-input:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 2px rgba(0,123,255,0.25);
}

.form-info {
  margin-top: 15px;
  padding: 15px;
  background-color: white;
  border-radius: 6px;
  border: 1px solid #dee2e6;
}

.form-info p {
  margin: 8px 0;
  font-family: monospace;
  font-size: 14px;
}

.event-log {
  margin-top: 30px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.event-log h3 {
  margin: 0 0 15px 0;
  color: #333;
}

.clear-btn {
  padding: 8px 16px;
  background-color: #dc3545;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-bottom: 15px;
}

.clear-btn:hover {
  background-color: #c82333;
}

.log-container {
  max-height: 300px;
  overflow-y: auto;
  background-color: white;
  border-radius: 6px;
  padding: 10px;
}

.log-entry {
  display: flex;
  padding: 8px;
  margin-bottom: 5px;
  border-bottom: 1px solid #f0f0f0;
  font-family: monospace;
  font-size: 13px;
}

.log-time {
  color: #6c757d;
  margin-right: 10px;
  min-width: 80px;
}

.log-type {
  color: #007bff;
  font-weight: bold;
  margin-right: 10px;
  min-width: 100px;
}

.log-details {
  color: #333;
  flex: 1;
}

@media (max-width: 768px) {
  .mouse-area {
    height: 150px;
    font-size: 16px;
  }
  
  .log-entry {
    flex-direction: column;
    gap: 5px;
  }
  
  .log-time,
  .log-type {
    min-width: auto;
  }
}
</style>

事件修饰符

在处理事件时调用 event.preventDefault()event.stopPropagation() 是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好。

为解决这一问题,Vue 为 v-on 提供了事件修饰符。修饰符是用 . 表示的指令后缀:

  • .stop
  • .prevent
  • .self
  • .capture
  • .once
  • .passive
vue
<template>
  <div class="modifiers-demo">
    <h2>事件修饰符演示</h2>
    
    <!-- .stop 修饰符 -->
    <div class="demo-section">
      <h3>.stop - 阻止事件冒泡</h3>
      <div @click="handleOuterClick" class="outer-box">
        外层容器 (点击我会触发)
        <div @click.stop="handleInnerClick" class="inner-box">
          内层容器 (点击我不会冒泡)
        </div>
        <div @click="handleInnerClick" class="inner-box">
          内层容器 (点击我会冒泡)
        </div>
      </div>
    </div>
    
    <!-- .prevent 修饰符 -->
    <div class="demo-section">
      <h3>.prevent - 阻止默认行为</h3>
      <form @submit.prevent="handleFormSubmit" class="demo-form">
        <input v-model="formInput" placeholder="输入一些内容">
        <button type="submit">提交 (阻止默认提交)</button>
      </form>
      
      <div class="link-demo">
        <a href="https://vuejs.org" @click.prevent="handleLinkClick">
          点击我不会跳转 (阻止默认行为)
        </a>
        <br>
        <a href="https://vuejs.org" @click="handleLinkClick">
          点击我会跳转 (正常行为)
        </a>
      </div>
    </div>
    
    <!-- .self 修饰符 -->
    <div class="demo-section">
      <h3>.self - 只在事件从元素本身触发时执行</h3>
      <div @click.self="handleSelfClick" class="self-container">
        只有点击这个容器本身才会触发事件
        <button @click="handleButtonClick" class="inner-button">
          点击按钮不会触发容器事件
        </button>
        <div class="inner-content">
          点击这个内容区域也不会触发容器事件
        </div>
      </div>
    </div>
    
    <!-- .once 修饰符 -->
    <div class="demo-section">
      <h3>.once - 事件只触发一次</h3>
      <button @click.once="handleOnceClick" class="once-button">
        只能点击一次的按钮 ({{ onceClickCount }})
      </button>
      <button @click="resetOnceButton" class="reset-button">
        重置
      </button>
    </div>
    
    <!-- .capture 修饰符 -->
    <div class="demo-section">
      <h3>.capture - 捕获阶段触发</h3>
      <div @click.capture="handleCaptureOuter" class="capture-outer">
        外层 (捕获阶段)
        <div @click="handleCaptureInner" class="capture-inner">
          内层 (冒泡阶段)
        </div>
      </div>
    </div>
    
    <!-- 修饰符链式调用 -->
    <div class="demo-section">
      <h3>修饰符链式调用</h3>
      <div @click="handleChainOuter" class="chain-outer">
        外层容器
        <a 
          href="https://vuejs.org" 
          @click.stop.prevent="handleChainLink"
          class="chain-link"
        >
          链式修饰符: .stop.prevent
        </a>
      </div>
    </div>
    
    <!-- 事件日志 -->
    <div class="event-log">
      <h3>事件日志</h3>
      <button @click="clearEventLog" class="clear-btn">清空日志</button>
      <div class="log-container">
        <div 
          v-for="(log, index) in eventLog" 
          :key="index"
          class="log-entry"
        >
          <span class="log-time">{{ log.time }}</span>
          <span class="log-event">{{ log.event }}</span>
          <span class="log-details">{{ log.details }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const formInput = ref('')
const onceClickCount = ref(0)
const eventLog = ref([])

function addEventLog(event, details) {
  eventLog.value.unshift({
    time: new Date().toLocaleTimeString(),
    event,
    details
  })
  
  if (eventLog.value.length > 30) {
    eventLog.value.pop()
  }
}

function handleOuterClick() {
  addEventLog('外层点击', '事件冒泡到外层容器')
}

function handleInnerClick() {
  addEventLog('内层点击', '内层容器被点击')
}

function handleFormSubmit() {
  addEventLog('表单提交', `表单内容: "${formInput.value}"`)
  alert(`表单提交: ${formInput.value}`)
}

function handleLinkClick() {
  addEventLog('链接点击', '链接被点击')
}

function handleSelfClick() {
  addEventLog('Self 点击', '只有点击容器本身才触发')
}

function handleButtonClick() {
  addEventLog('按钮点击', '内部按钮被点击')
}

function handleOnceClick() {
  onceClickCount.value++
  addEventLog('Once 点击', `第 ${onceClickCount.value} 次点击 (只能触发一次)`)
}

function resetOnceButton() {
  // 注意:.once 修饰符在组件重新渲染时会重置
  // 这里我们通过改变 key 来强制重新渲染
  onceClickCount.value = 0
  addEventLog('重置', 'Once 按钮已重置')
}

function handleCaptureOuter() {
  addEventLog('捕获外层', '在捕获阶段触发')
}

function handleCaptureInner() {
  addEventLog('冒泡内层', '在冒泡阶段触发')
}

function handleChainOuter() {
  addEventLog('链式外层', '外层容器点击')
}

function handleChainLink() {
  addEventLog('链式链接', '链接点击 (阻止冒泡和默认行为)')
}

function clearEventLog() {
  eventLog.value = []
}
</script>

<style scoped>
.modifiers-demo {
  max-width: 900px;
  margin: 0 auto;
  padding: 20px;
}

.demo-section {
  margin-bottom: 25px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.demo-section h3 {
  margin: 0 0 15px 0;
  color: #333;
}

.outer-box {
  padding: 20px;
  background-color: #e3f2fd;
  border-radius: 6px;
  font-weight: bold;
  color: #1976d2;
}

.key-log {
  margin-top: 30px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.key-log h3 {
  margin: 0 0 15px 0;
  color: #333;
}

.clear-btn {
  padding: 8px 16px;
  background-color: #dc3545;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-bottom: 15px;
}

.clear-btn:hover {
  background-color: #c82333;
}

.log-container {
  max-height: 200px;
  overflow-y: auto;
  background-color: white;
  border-radius: 6px;
  padding: 10px;
}

.log-entry {
  display: flex;
  padding: 6px;
  margin-bottom: 3px;
  border-bottom: 1px solid #f0f0f0;
  font-family: monospace;
  font-size: 13px;
}

.log-time {
  color: #6c757d;
  margin-right: 10px;
  min-width: 80px;
}

.log-key {
  color: #007bff;
  font-weight: bold;
  margin-right: 10px;
  min-width: 100px;
}

.log-action {
  color: #333;
  flex: 1;
}

@media (max-width: 768px) {
  .game-area {
    width: 280px;
    height: 280px;
  }
  
  .color-input,
  .letter-input {
    width: 100%;
  }
  
  .color-legend,
  .key-legend {
    flex-direction: column;
    gap: 5px;
  }
  
  .log-entry {
    flex-direction: column;
    gap: 3px;
  }
  
  .log-time,
  .log-key {
    min-width: auto;
  }
}
</style>

使用按键修饰符时需要注意:

  • 修饰符可以链式调用:@keydown.ctrl.shift.enter
  • 系统修饰键(ctrlaltshiftmeta)必须与其他键组合使用
  • 可以使用 keyCode@keydown.13(不推荐,已废弃)
  • 推荐使用 key 值:@keydown.page-down

鼠标按键修饰符

  • .left - 只有左键触发
  • .right - 只有右键触发
  • .middle - 只有中键触发
vue
<template>
  <div class="mouse-button-demo">
    <h2>鼠标按键修饰符</h2>
    
    <div class="demo-area">
      <div 
        @click.left="handleLeftClick"
        @click.right.prevent="handleRightClick"
        @click.middle="handleMiddleClick"
        class="click-area"
      >
        <h3>点击测试区域</h3>
        <p>左键:普通点击</p>
        <p>右键:右键菜单(已阻止)</p>
        <p>中键:中键点击</p>
      </div>
      
      <div class="click-log">
        <h4>点击日志:</h4>
        <ul>
          <li v-for="(log, index) in clickLogs" :key="index">
            {{ log }}
          </li>
        </ul>
        <button @click="clearClickLogs">清空日志</button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const clickLogs = ref([])

function addClickLog(message) {
  clickLogs.value.unshift(`${new Date().toLocaleTimeString()}: ${message}`)
  if (clickLogs.value.length > 10) {
    clickLogs.value.pop()
  }
}

function handleLeftClick() {
  addClickLog('左键点击')
}

function handleRightClick() {
  addClickLog('右键点击(阻止了默认菜单)')
}

function handleMiddleClick() {
  addClickLog('中键点击')
}

function clearClickLogs() {
  clickLogs.value = []
}
</script>

<style scoped>
.mouse-button-demo {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.demo-area {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
}

.click-area {
  padding: 30px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border-radius: 10px;
  text-align: center;
  cursor: pointer;
  user-select: none;
}

.click-area h3 {
  margin: 0 0 15px 0;
}

.click-area p {
  margin: 8px 0;
  font-size: 14px;
}

.click-log {
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.click-log h4 {
  margin: 0 0 10px 0;
}

.click-log ul {
  list-style: none;
  padding: 0;
  margin: 0 0 15px 0;
  max-height: 200px;
  overflow-y: auto;
}

.click-log li {
  padding: 5px;
  margin-bottom: 3px;
  background-color: white;
  border-radius: 4px;
  font-family: monospace;
  font-size: 12px;
}

.click-log button {
  padding: 8px 16px;
  background-color: #dc3545;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

@media (max-width: 768px) {
  .demo-area {
    grid-template-columns: 1fr;
  }
}
</style>

最佳实践

  1. 优先使用方法事件处理器:将复杂逻辑放在方法中,保持模板简洁
  2. 合理使用事件修饰符:减少在方法中手动调用 preventDefault()stopPropagation()
  3. 按键修饰符的选择:优先使用语义化的按键名而不是 keyCode
  4. 性能考虑:避免在模板中使用复杂的内联表达式
  5. 事件委托:对于大量相似元素,考虑使用事件委托

下一步


## 按键修饰符

在监听键盘事件时,我们经常需要检查特定的按键。Vue 允许在 `v-on` 或 `@` 监听键盘事件时添加按键修饰符。

```vue
<template>
  <div class="key-modifiers-demo">
    <h2>按键修饰符演示</h2>
    
    <!-- 基本按键修饰符 -->
    <div class="demo-section">
      <h3>基本按键修饰符</h3>
      <div class="input-group">
        <label>按 Enter 键提交:</label>
        <input 
          v-model="enterInput" 
          @keyup.enter="handleEnter"
          placeholder="输入后按 Enter"
        >
      </div>
      
      <div class="input-group">
        <label>按 Escape 键清空:</label>
        <input 
          v-model="escapeInput" 
          @keyup.esc="handleEscape"
          placeholder="输入后按 Esc 清空"
        >
      </div>
      
      <div class="input-group">
        <label>按 Tab 键切换:</label>
        <input 
          v-model="tabInput" 
          @keydown.tab="handleTab"
          placeholder="按 Tab 键"
        >
      </div>
      
      <div class="input-group">
        <label>按空格键计数:</label>
        <input 
          v-model="spaceInput" 
          @keyup.space="handleSpace"
          placeholder="按空格键计数"
        >
        <span class="counter">空格次数: {{ spaceCount }}</span>
      </div>
    </div>
    
    <!-- 方向键 -->
    <div class="demo-section">
      <h3>方向键控制</h3>
      <div class="arrow-demo">
        <div class="instructions">使用方向键移动小方块:</div>
        <div 
          class="game-area"
          tabindex="0"
          @keydown.up.prevent="moveUp"
          @keydown.down.prevent="moveDown"
          @keydown.left.prevent="moveLeft"
          @keydown.right.prevent="moveRight"
        >
          <div 
            class="player"
            :style="{ 
              left: playerX + 'px', 
              top: playerY + 'px' 
            }"
          ></div>
        </div>
        <div class="position-info">
          位置: ({{ playerX }}, {{ playerY }})
        </div>
      </div>
    </div>
    
    <!-- 修饰键组合 -->
    <div class="demo-section">
      <h3>修饰键组合</h3>
      <div class="modifier-inputs">
        <div class="input-group">
          <label>Ctrl + Enter 保存:</label>
          <textarea 
            v-model="ctrlEnterText" 
            @keydown.ctrl.enter="handleCtrlEnter"
            placeholder="输入内容后按 Ctrl+Enter 保存"
            rows="3"
          ></textarea>
        </div>
        
        <div class="input-group">
          <label>Shift + Delete 删除:</label>
          <input 
            v-model="shiftDeleteText" 
            @keydown.shift.delete="handleShiftDelete"
            placeholder="按 Shift+Delete 删除"
          >
        </div>
        
        <div class="input-group">
          <label>Alt + S 搜索:</label>
          <input 
            v-model="altSText" 
            @keydown.alt.s.prevent="handleAltS"
            placeholder="按 Alt+S 搜索"
          >
        </div>
      </div>
    </div>
    
    <!-- 数字键 -->
    <div class="demo-section">
      <h3>数字键快捷操作</h3>
      <div class="number-demo">
        <div class="instructions">按数字键 1-5 选择颜色:</div>
        <div class="number-inputs">
          <input 
            v-model="numberInput" 
            @keydown.1="() => selectColor('red')"
            @keydown.2="() => selectColor('green')"
            @keydown.3="() => selectColor('blue')"
            @keydown.4="() => selectColor('yellow')"
            @keydown.5="() => selectColor('purple')"
            placeholder="聚焦后按数字键 1-5"
            class="color-input"
          >
        </div>
        <div class="color-display" :style="{ backgroundColor: selectedColor }">
          当前颜色: {{ selectedColor }}
        </div>
        <div class="color-legend">
          <span>1-红色</span>
          <span>2-绿色</span>
          <span>3-蓝色</span>
          <span>4-黄色</span>
          <span>5-紫色</span>
        </div>
      </div>
    </div>
    
    <!-- 自定义按键别名 -->
    <div class="demo-section">
      <h3>字母键快捷操作</h3>
      <div class="letter-demo">
        <div class="instructions">按字母键执行操作:</div>
        <input 
          v-model="letterInput" 
          @keydown.q="handleQ"
          @keydown.w="handleW"
          @keydown.e="handleE"
          @keydown.r="handleR"
          placeholder="聚焦后按 Q/W/E/R 键"
          class="letter-input"
        >
        <div class="action-display">
          最后操作: {{ lastAction }}
        </div>
        <div class="key-legend">
          <span>Q-查询</span>
          <span>W-写入</span>
          <span>E-编辑</span>
          <span>R-刷新</span>
        </div>
      </div>
    </div>
    
    <!-- 按键事件日志 -->
    <div class="key-log">
      <h3>按键事件日志</h3>
      <button @click="clearKeyLog" class="clear-btn">清空日志</button>
      <div class="log-container">
        <div 
          v-for="(log, index) in keyLog" 
          :key="index"
          class="log-entry"
        >
          <span class="log-time">{{ log.time }}</span>
          <span class="log-key">{{ log.key }}</span>
          <span class="log-action">{{ log.action }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const enterInput = ref('')
const escapeInput = ref('')
const tabInput = ref('')
const spaceInput = ref('')
const spaceCount = ref(0)
const playerX = ref(150)
const playerY = ref(150)
const ctrlEnterText = ref('')
const shiftDeleteText = ref('')
const altSText = ref('')
const numberInput = ref('')
const selectedColor = ref('#f0f0f0')
const letterInput = ref('')
const lastAction = ref('')
const keyLog = ref([])

function addKeyLog(key, action) {
  keyLog.value.unshift({
    time: new Date().toLocaleTimeString(),
    key,
    action
  })
  
  if (keyLog.value.length > 20) {
    keyLog.value.pop()
  }
}

function handleEnter() {
  addKeyLog('Enter', `提交内容: "${enterInput.value}"`)
  alert(`提交: ${enterInput.value}`)
  enterInput.value = ''
}

function handleEscape() {
  addKeyLog('Escape', '清空输入框')
  escapeInput.value = ''
}

function handleTab(event) {
  addKeyLog('Tab', '按下 Tab 键')
  // 注意:这里我们不阻止默认行为,让 Tab 正常工作
}

function handleSpace() {
  spaceCount.value++
  addKeyLog('Space', `空格计数: ${spaceCount.value}`)
}

function moveUp() {
  if (playerY.value > 0) {
    playerY.value -= 20
    addKeyLog('↑', `向上移动到 (${playerX.value}, ${playerY.value})`)
  }
}

function moveDown() {
  if (playerY.value < 280) {
    playerY.value += 20
    addKeyLog('↓', `向下移动到 (${playerX.value}, ${playerY.value})`)
  }
}

function moveLeft() {
  if (playerX.value > 0) {
    playerX.value -= 20
    addKeyLog('←', `向左移动到 (${playerX.value}, ${playerY.value})`)
  }
}

function moveRight() {
  if (playerX.value < 280) {
    playerX.value += 20
    addKeyLog('→', `向右移动到 (${playerX.value}, ${playerY.value})`)
  }
}

function handleCtrlEnter() {
  addKeyLog('Ctrl+Enter', `保存文本: "${ctrlEnterText.value}"`)
  alert(`保存: ${ctrlEnterText.value}`)
}

function handleShiftDelete() {
  addKeyLog('Shift+Delete', '删除输入内容')
  shiftDeleteText.value = ''
}

function handleAltS() {
  addKeyLog('Alt+S', `搜索: "${altSText.value}"`)
  alert(`搜索: ${altSText.value}`)
}

function selectColor(color) {
  const colorMap = {
    red: '#ff4444',
    green: '#44ff44',
    blue: '#4444ff',
    yellow: '#ffff44',
    purple: '#ff44ff'
  }
  selectedColor.value = colorMap[color]
  addKeyLog('数字键', `选择颜色: ${color}`)
}

function handleQ() {
  lastAction.value = '查询操作'
  addKeyLog('Q', '执行查询操作')
}

function handleW() {
  lastAction.value = '写入操作'
  addKeyLog('W', '执行写入操作')
}

function handleE() {
  lastAction.value = '编辑操作'
  addKeyLog('E', '执行编辑操作')
}

function handleR() {
  lastAction.value = '刷新操作'
  addKeyLog('R', '执行刷新操作')
}

function clearKeyLog() {
  keyLog.value = []
}
</script>

<style scoped>
.key-modifiers-demo {
  max-width: 900px;
  margin: 0 auto;
  padding: 20px;
}

.demo-section {
  margin-bottom: 25px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.demo-section h3 {
  margin: 0 0 15px 0;
  color: #333;
}

.input-group {
  margin-bottom: 15px;
}

.input-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
  color: #555;
}

.input-group input,
.input-group textarea {
  width: 100%;
  padding: 10px;
  border: 2px solid #ced4da;
  border-radius: 6px;
  font-size: 16px;
  transition: border-color 0.3s;
  box-sizing: border-box;
}

.input-group input:focus,
.input-group textarea:focus {
  outline: none;
  border-color: #007bff;
}

.counter {
  margin-left: 10px;
  font-weight: bold;
  color: #007bff;
}

.arrow-demo {
  text-align: center;
}

.instructions {
  margin-bottom: 15px;
  font-weight: bold;
  color: #555;
}

.game-area {
  width: 320px;
  height: 320px;
  border: 3px solid #333;
  border-radius: 8px;
  position: relative;
  margin: 0 auto 10px;
  background: linear-gradient(45deg, #f0f0f0 25%, transparent 25%), 
              linear-gradient(-45deg, #f0f0f0 25%, transparent 25%), 
              linear-gradient(45deg, transparent 75%, #f0f0f0 75%), 
              linear-gradient(-45deg, transparent 75%, #f0f0f0 75%);
  background-size: 20px 20px;
  background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}

.game-area:focus {
  outline: 3px solid #007bff;
}

.player {
  width: 20px;
  height: 20px;
  background-color: #ff4444;
  border-radius: 50%;
  position: absolute;
  transition: all 0.2s ease;
  box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}

.position-info {
  font-family: monospace;
  color: #666;
}

.modifier-inputs {
  display: grid;
  gap: 15px;
}

.number-demo,
.letter-demo {
  text-align: center;
}

.color-input,
.letter-input {
  width: 300px;
  margin: 10px auto;
  display: block;
}

.color-display {
  width: 200px;
  height: 60px;
  margin: 15px auto;
  border: 2px solid #333;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
  color: #333;
  text-shadow: 1px 1px 1px rgba(255,255,255,0.8);
}

.color-legend,
.key-legend {
  display: flex;
  justify-content: center;
  gap: 15px;
  margin-top: 10px;
}

.color-legend span,
.key-legend span {
  padding: 5px 10px;
  background-color: #e9ecef;
  border-radius: 4px;
  font-size: 14px;
  font-weight: bold;
}

.action-display {
  margin: 15px 0;
  padding: 10px;
  background-color: #e3f2fd;
  border: 1px solid #bbdefb;
  border-radius: 4px;
  font-family: 'Courier New', monospace;
  font-size: 14px;
  color: #1565c0;
}

.key-log {
  margin-top: 30px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 8px;
  border: 1px solid #dee2e6;
}

.key-log h3 {
  margin: 0 0 15px 0;
  color: #333;
}

.clear-btn {
  padding: 8px 16px;
  background-color: #dc3545;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  margin-bottom: 15px;
}

.clear-btn:hover {
  background-color: #c82333;
}

.log-container {
  max-height: 250px;
  overflow-y: auto;
  background-color: white;
  border-radius: 6px;
  padding: 10px;
}

.log-entry {
  display: flex;
  padding: 6px;
  margin-bottom: 3px;
  border-bottom: 1px solid #f0f0f0;
  font-family: monospace;
  font-size: 13px;
}

.log-time {
  color: #6c757d;
  margin-right: 10px;
  min-width: 80px;
}

.log-key {
  color: #007bff;
  font-weight: bold;
  margin-right: 10px;
  min-width: 100px;
}

.log-action {
  color: #333;
  flex: 1;
}

@media (max-width: 768px) {
  .game-area {
    width: 280px;
    height: 280px;
  }
  
  .color-input,
  .letter-input {
    width: 100%;
    max-width: 300px;
  }
  
  .log-entry {
    flex-direction: column;
    gap: 3px;
  }
  
  .log-time,
  .log-key {
    min-width: auto;
  }
}
</style>

基于 Vue.js 官方文档构建的学习宝典