事件处理
在 Vue 中,我们可以使用 v-on
指令(简写为 @
)来监听 DOM 事件,并在事件触发时执行 JavaScript 代码。
监听事件
内联事件处理器
事件处理器的值可以是:
- 内联事件处理器:事件被触发时执行的内联 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>
方法事件处理器
- 方法事件处理器:一个指向组件上定义的方法的属性名或是路径
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
- 系统修饰键(
ctrl
、alt
、shift
、meta
)必须与其他键组合使用 - 可以使用
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>
最佳实践
- 优先使用方法事件处理器:将复杂逻辑放在方法中,保持模板简洁
- 合理使用事件修饰符:减少在方法中手动调用
preventDefault()
和stopPropagation()
- 按键修饰符的选择:优先使用语义化的按键名而不是 keyCode
- 性能考虑:避免在模板中使用复杂的内联表达式
- 事件委托:对于大量相似元素,考虑使用事件委托
下一步
## 按键修饰符
在监听键盘事件时,我们经常需要检查特定的按键。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>