事件处理
在uni-app中,事件处理是实现用户交互的重要机制。本文将详细介绍uni-app中的事件处理方式、事件类型以及最佳实践。
事件绑定
基本语法
uni-app沿用了Vue的事件处理语法,使用v-on
指令(简写为@
)来监听DOM事件:
<!-- 完整语法 -->
<button v-on:tap="handleTap">点击我</button>
<!-- 缩写语法 -->
<button @tap="handleTap">点击我</button>
事件处理方法定义在组件的methods
选项中:
export default {
methods: {
handleTap() {
console.log('按钮被点击了')
}
}
}
内联事件处理
对于简单的事件处理逻辑,可以直接在模板中使用内联JavaScript语句:
<button @tap="counter += 1">计数器: {{ counter }}</button>
事件传参
在事件处理方法中,可以传入自定义参数:
<button @tap="handleTap('hello', $event)">带参数的事件</button>
methods: {
handleTap(message, event) {
console.log(message) // 'hello'
console.log(event) // 原生事件对象
}
}
提示
使用$event
可以在传入自定义参数的同时,获取到原生的事件对象。
常用事件类型
uni-app支持多种事件类型,以下是一些常用的事件:
触摸事件
- tap:点击事件,类似于HTML中的click事件
- longpress:长按事件,手指长时间触摸
- touchstart:触摸开始事件
- touchmove:触摸移动事件
- touchend:触摸结束事件
- touchcancel:触摸取消事件
<view @tap="handleTap">点击</view>
<view @longpress="handleLongPress">长按</view>
<view
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
触摸区域
</view>
表单事件
- input:输入框内容变化时触发
- focus:输入框聚焦时触发
- blur:输入框失去焦点时触发
- change:选择器、开关等值变化时触发
- submit:表单提交时触发
<input
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
placeholder="请输入内容"
/>
<picker
@change="handleChange"
:range="['选项1', '选项2', '选项3']"
>
<view>当前选择: {{ currentSelection }}</view>
</picker>
<form @submit="handleSubmit">
<!-- 表单内容 -->
<button form-type="submit">提交</button>
</form>
生命周期事件
- load:页面加载时触发
- ready:页面初次渲染完成时触发
- show:页面显示时触发
- hide:页面隐藏时触发
这些事件通常在页面的生命周期钩子函数中处理:
export default {
onLoad(options) {
console.log('页面加载', options)
},
onReady() {
console.log('页面初次渲染完成')
},
onShow() {
console.log('页面显示')
},
onHide() {
console.log('页面隐藏')
}
}
滚动事件
- scroll:滚动时触发
<scroll-view
scroll-y
@scroll="handleScroll"
style="height: 300px;"
>
<view v-for="item in items" :key="item.id">
{{ item.text }}
</view>
</scroll-view>
methods: {
handleScroll(e) {
console.log('滚动位置', e.detail)
// e.detail.scrollTop 垂直滚动位置
// e.detail.scrollLeft 水平滚动位置
// e.detail.scrollHeight 滚动内容高度
// e.detail.scrollWidth 滚动内容宽度
}
}
事件修饰符
uni-app支持Vue的事件修饰符,用于处理事件的细节行为:
事件传播修饰符
- .stop:阻止事件冒泡
- .prevent:阻止事件的默认行为
- .capture:使用事件捕获模式
- .self:只当事件在该元素本身触发时才触发处理函数
- .once:事件只触发一次
<!-- 阻止事件冒泡 -->
<view @tap.stop="handleTap">阻止冒泡</view>
<!-- 阻止默认行为 -->
<form @submit.prevent="handleSubmit">
<!-- 表单内容 -->
</form>
<!-- 只触发一次 -->
<button @tap.once="handleTap">只能点击一次</button>
按键修饰符
在处理键盘事件时,可以使用按键修饰符:
<!-- 按下回车键时提交表单 -->
<input @keyup.enter="submit" />
<!-- 按下Esc键时取消操作 -->
<input @keyup.esc="cancel" />
注意
按键修饰符主要在H5平台有效,在小程序和App平台可能需要使用其他方式处理键盘事件。
事件对象
事件处理函数会自动接收一个事件对象(event),包含事件的相关信息:
<view @tap="handleTap">点击获取事件信息</view>
methods: {
handleTap(event) {
console.log(event)
// 常用属性
console.log(event.type) // 事件类型,如 'tap'
console.log(event.target) // 触发事件的元素
console.log(event.currentTarget) // 当前处理事件的元素
console.log(event.timeStamp) // 事件触发的时间戳
// 触摸事件特有属性
if (event.touches) {
console.log(event.touches) // 当前屏幕上的所有触摸点
console.log(event.changedTouches) // 触发当前事件的触摸点
}
// 表单事件特有属性
if (event.detail && event.detail.value !== undefined) {
console.log(event.detail.value) // 表单组件的值
}
}
}
事件对象的跨平台差异
不同平台(小程序、H5、App)的事件对象可能存在差异,建议使用以下方式获取通用属性:
methods: {
handleInput(event) {
// 获取输入值
const value = event.detail.value || event.target.value
// 获取dataset
const dataset = event.currentTarget.dataset
}
}
自定义事件
组件间通信
在自定义组件中,可以使用$emit
方法触发自定义事件,实现子组件向父组件通信:
子组件(child.vue):
<template>
<view>
<button @tap="sendMessage">发送消息</button>
</view>
</template>
<script>
export default {
methods: {
sendMessage() {
// 触发自定义事件,并传递数据
this.$emit('message', {
content: '这是来自子组件的消息',
time: new Date()
})
}
}
}
</script>
父组件:
<template>
<view>
<!-- 监听子组件的自定义事件 -->
<child @message="handleMessage"></child>
<view v-if="messageReceived">
收到消息: {{ message.content }}
时间: {{ formatTime(message.time) }}
</view>
</view>
</template>
<script>
import Child from './child.vue'
export default {
components: {
Child
},
data() {
return {
messageReceived: false,
message: null
}
},
methods: {
handleMessage(msg) {
this.messageReceived = true
this.message = msg
},
formatTime(date) {
return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`
}
}
}
</script>
事件总线(EventBus)
对于非父子组件间的通信,可以使用事件总线:
// eventBus.js
import Vue from 'vue'
export const eventBus = new Vue()
// 在Vue 3中可以使用mitt库
// import mitt from 'mitt'
// export const eventBus = mitt()
组件A(发送事件):
import { eventBus } from '@/utils/eventBus'
export default {
methods: {
sendGlobalMessage() {
eventBus.$emit('global-message', {
from: 'ComponentA',
content: '全局消息'
})
}
}
}
组件B(接收事件):
import { eventBus } from '@/utils/eventBus'
export default {
data() {
return {
messages: []
}
},
created() {
// 监听全局事件
eventBus.$on('global-message', this.receiveMessage)
},
beforeDestroy() {
// 组件销毁前移除事件监听
eventBus.$off('global-message', this.receiveMessage)
},
methods: {
receiveMessage(msg) {
this.messages.push(msg)
}
}
}
手势识别
uni-app提供了基础的触摸事件,但对于复杂的手势识别(如滑动、捏合、旋转等),可以使用以下方法:
自定义手势识别
<template>
<view
class="gesture-area"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
<text>{{ gestureInfo }}</text>
</view>
</template>
<script>
export default {
data() {
return {
gestureInfo: '请在此区域进行手势操作',
startX: 0,
startY: 0,
endX: 0,
endY: 0,
startTime: 0,
isSwiping: false
}
},
methods: {
handleTouchStart(e) {
const touch = e.touches[0]
this.startX = touch.clientX
this.startY = touch.clientY
this.startTime = Date.now()
this.isSwiping = true
},
handleTouchMove(e) {
if (!this.isSwiping) return
const touch = e.touches[0]
this.endX = touch.clientX
this.endY = touch.clientY
// 计算移动距离
const deltaX = this.endX - this.startX
const deltaY = this.endY - this.startY
// 显示实时移动信息
this.gestureInfo = `移动: X=${deltaX.toFixed(2)}, Y=${deltaY.toFixed(2)}`
},
handleTouchEnd(e) {
if (!this.isSwiping) return
this.isSwiping = false
// 计算最终移动距离和方向
const deltaX = this.endX - this.startX
const deltaY = this.endY - this.startY
const duration = Date.now() - this.startTime
// 判断手势类型
if (Math.abs(deltaX) > 50 || Math.abs(deltaY) > 50) {
// 判断方向
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// 水平方向
const direction = deltaX > 0 ? '右' : '左'
this.gestureInfo = `水平滑动: ${direction}, 距离: ${Math.abs(deltaX).toFixed(2)}, 时间: ${duration}ms`
} else {
// 垂直方向
const direction = deltaY > 0 ? '下' : '上'
this.gestureInfo = `垂直滑动: ${direction}, 距离: ${Math.abs(deltaY).toFixed(2)}, 时间: ${duration}ms`
}
} else if (duration < 300 && Math.abs(deltaX) < 10 && Math.abs(deltaY) < 10) {
this.gestureInfo = '轻触'
} else {
this.gestureInfo = '未识别的手势'
}
}
}
}
</script>
<style>
.gesture-area {
width: 100%;
height: 300rpx;
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
font-size: 28rpx;
}
</style>
使用第三方手势库
对于更复杂的手势识别,可以使用第三方库,如hammerjs
(在H5平台):
# 安装hammerjs
npm install hammerjs
<template>
<view ref="gestureElement" class="gesture-area">
<text>{{ gestureInfo }}</text>
</view>
</template>
<script>
// 仅在H5平台引入
let Hammer = null
if (process.env.UNI_PLATFORM === 'h5') {
Hammer = require('hammerjs')
}
export default {
data() {
return {
gestureInfo: '请在此区域进行手势操作',
hammer: null
}
},
mounted() {
// 仅在H5平台初始化Hammer
if (Hammer) {
this.$nextTick(() => {
const element = this.$refs.gestureElement
this.hammer = new Hammer(element)
// 配置识别器
this.hammer.get('swipe').set({ direction: Hammer.DIRECTION_ALL })
this.hammer.get('pinch').set({ enable: true })
this.hammer.get('rotate').set({ enable: true })
// 监听手势事件
this.hammer.on('tap', (e) => {
this.gestureInfo = '轻触'
})
this.hammer.on('swipe', (e) => {
const direction = this.getDirection(e.direction)
this.gestureInfo = `滑动: ${direction}, 速度: ${e.velocity.toFixed(2)}`
})
this.hammer.on('pinch', (e) => {
this.gestureInfo = `捏合: 比例 ${e.scale.toFixed(2)}`
})
this.hammer.on('rotate', (e) => {
this.gestureInfo = `旋转: ${e.rotation.toFixed(2)}度`
})
})
}
},
beforeDestroy() {
// 销毁Hammer实例
if (this.hammer) {
this.hammer.destroy()
this.hammer = null
}
},
methods: {
getDirection(direction) {
switch(direction) {
case Hammer.DIRECTION_LEFT: return '左'
case Hammer.DIRECTION_RIGHT: return '右'
case Hammer.DIRECTION_UP: return '上'
case Hammer.DIRECTION_DOWN: return '下'
default: return '未知'
}
}
}
}
</script>
事件委托
事件委托(Event Delegation)是一种常用的事件处理模式,通过将事件监听器添加到父元素,而不是每个子元素,可以提高性能并简化代码:
<template>
<view class="list" @tap="handleItemClick">
<view
v-for="item in items"
:key="item.id"
class="list-item"
:data-id="item.id"
>
{{ item.text }}
</view>
</view>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, text: '项目1' },
{ id: 2, text: '项目2' },
{ id: 3, text: '项目3' },
{ id: 4, text: '项目4' },
{ id: 5, text: '项目5' }
]
}
},
methods: {
handleItemClick(e) {
// 获取被点击元素的dataset
const dataset = e.target.dataset || e.currentTarget.dataset
if (dataset.id) {
const id = Number(dataset.id)
const item = this.items.find(item => item.id === id)
if (item) {
uni.showToast({
title: `点击了: ${item.text}`,
icon: 'none'
})
}
}
}
}
}
</script>
性能优化
防抖与节流
对于频繁触发的事件(如滚动、输入、调整窗口大小等),应使用防抖(Debounce)或节流(Throttle)技术来优化性能:
// utils/event.js
// 防抖函数
export function debounce(func, wait = 300) {
let timeout
return function(...args) {
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
// 节流函数
export function throttle(func, wait = 300) {
let timeout = null
let previous = 0
return function(...args) {
const now = Date.now()
const remaining = wait - (now - previous)
if (remaining <= 0) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
func.apply(this, args)
} else if (!timeout) {
timeout = setTimeout(() => {
previous = Date.now()
timeout = null
func.apply(this, args)
}, remaining)
}
}
}
在组件中使用:
<template>
<view>
<input @input="handleInput" placeholder="搜索..." />
<scroll-view
scroll-y
@scroll="handleScroll"
style="height: 300px;"
>
<view v-for="item in items" :key="item.id">
{{ item.text }}
</view>
</scroll-view>
</view>
</template>
<script>
import { debounce, throttle } from '@/utils/event'
export default {
data() {
return {
items: [],
searchText: ''
}
},
created() {
// 创建防抖和节流函数
this.debouncedSearch = debounce(this.search, 500)
this.throttledScroll = throttle(this.onScroll, 200)
},
methods: {
// 输入事件使用防抖
handleInput(e) {
const value = e.detail.value || e.target.value
this.searchText = value
this.debouncedSearch(value)
},
search(text) {
console.log('执行搜索:', text)
// 实际搜索逻辑
},
// 滚动事件使用节流
handleScroll(e) {
this.throttledScroll(e)
},
onScroll(e) {
console.log('处理滚动:', e.detail.scrollTop)
// 实际滚动处理逻辑
}
}
}
</script>
避免内联函数
在模板中使用内联函数会导致每次重新渲染时创建新的函数实例,应尽量避免:
<!-- 不推荐 -->
<view v-for="item in items" :key="item.id" @tap="() => handleItemClick(item.id)">
{{ item.text }}
</view>
<!-- 推荐 -->
<view v-for="item in items" :key="item.id" @tap="handleItemClick" :data-id="item.id">
{{ item.text }}
</view>
methods: {
handleItemClick(e) {
const id = e.currentTarget.dataset.id
// 处理点击逻辑
}
}
使用计算属性代替方法
对于在模板中多次使用的数据转换,应使用计算属性而不是方法:
<!-- 不推荐 -->
<view v-for="item in items" :key="item.id">
{{ formatPrice(item.price) }}
</view>
<!-- 推荐 -->
<view v-for="item in formattedItems" :key="item.id">
{{ item.formattedPrice }}
</view>
computed: {
formattedItems() {
return this.items.map(item => ({
...item,
formattedPrice: this.formatPrice(item.price)
}))
}
},
methods: {
formatPrice(price) {
return '¥' + price.toFixed(2)
}
}
跨平台注意事项
事件命名差异
不同平台对事件的命名可能存在差异,例如:
- Web平台使用
click
,而小程序使用tap
- Web平台使用
change
,而小程序的某些组件可能使用bindchange
为了保持一致性,uni-app做了统一处理,建议使用uni-app推荐的事件名:
<!-- 在所有平台都使用tap事件 -->
<view @tap="handleTap">点击</view>
<!-- 在所有平台都使用change事件 -->
<picker @change="handleChange" :range="options">选择</picker>
事件对象差异
不同平台的事件对象结构可能不同,例如:
- Web平台通过
event.target.value
获取输入值 - 小程序通过
event.detail.value
获取输入值
为了处理这些差异,可以编写兼容性代码:
methods: {
handleInput(event) {
// 兼容不同平台
const value = event.detail.value || (event.target && event.target.value) || ''
this.inputValue = value
}
}
事件冒泡差异
不同平台的事件冒泡机制可能存在差异,特别是在自定义组件嵌套时:
<!-- 父组件 -->
<view @tap="handleOuterTap">
<custom-component @tap="handleInnerTap"></custom-component>
</view>
在某些平台上,点击自定义组件可能会同时触发内部和外部的tap事件。为了确保一致的行为,可以使用.stop
修饰符:
<view @tap="handleOuterTap">
<custom-component @tap.stop="handleInnerTap"></custom-component>
</view>
实际应用示例
拖拽排序列表
<template>
<view class="drag-list">
<view
v-for="(item, index) in items"
:key="item.id"
class="drag-item"
:class="{ 'dragging': draggingIndex === index }"
:style="getItemStyle(index)"
@touchstart="handleTouchStart($event, index)"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
<text>{{ item.text }}</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, text: '项目1' },
{ id: 2, text: '项目2' },
{ id: 3, text: '项目3' },
{ id: 4, text: '项目4' },
{ id: 5, text: '项目5' }
],
draggingIndex: -1,
startY: 0,
currentY: 0,
itemHeight: 0,
positions: []
}
},
mounted() {
// 获取项目高度
const query = uni.createSelectorQuery().in(this)
query.select('.drag-item').boundingClientRect(data => {
if (data) {
this.itemHeight = data.height
// 初始化位置数组
this.positions = this.items.map((_, index) => index * this.itemHeight)
}
}).exec()
},
methods: {
handleTouchStart(e, index) {
this.draggingIndex = index
this.startY = e.touches[0].clientY
this.currentY = this.positions[index]
},
handleTouchMove(e) {
if (this.draggingIndex < 0) return
const moveY = e.touches[0].clientY - this.startY
this.positions[this.draggingIndex] = this.currentY + moveY
// 检查是否需要交换位置
const currentPos = this.positions[this.draggingIndex]
let targetIndex = -1
// 向下拖动
if (moveY > 0 && this.draggingIndex < this.items.length - 1) {
const nextPos = (this.draggingIndex + 1) * this.itemHeight
if (currentPos > nextPos) {
targetIndex = this.draggingIndex + 1
}
}
// 向上拖动
else if (moveY < 0 && this.draggingIndex > 0) {
const prevPos = (this.draggingIndex - 1) * this.itemHeight
if (currentPos < prevPos) {
targetIndex = this.draggingIndex - 1
}
}
// 交换位置
if (targetIndex >= 0) {
this.swapItems(this.draggingIndex, targetIndex)
this.draggingIndex = targetIndex
}
},
handleTouchEnd() {
if (this.draggingIndex < 0) return
// 重置位置
this.positions = this.items.map((_, index) => index * this.itemHeight)
this.draggingIndex = -1
},
swapItems(fromIndex, toIndex) {
// 交换数组中的项目
const temp = this.items[fromIndex]
this.$set(this.items, fromIndex, this.items[toIndex])
this.$set(this.items, toIndex, temp)
// 交换位置数组中的值
const tempPos = this.positions[fromIndex]
this.$set(this.positions, fromIndex, this.positions[toIndex])
this.$set(this.positions, toIndex, tempPos)
},
getItemStyle(index) {
if (index === this.draggingIndex) {
return {
transform: `translateY(${this.positions[index]}px)`,
zIndex: 10,
transition: 'none'
}
}
return {
transform: `translateY(${this.positions[index]}px)`,
transition: 'transform 0.2s ease'
}
}
}
}
</script>
<style>
.drag-list {
padding: 20rpx;
position: relative;
height: 600rpx;
}
.drag-item {
height: 100rpx;
background-color: #ffffff;
border: 1rpx solid #eeeeee;
border-radius: 8rpx;
margin-bottom: 20rpx;
padding: 0 30rpx;
display: flex;
align-items: center;
position: absolute;
left: 20rpx;
right: 20rpx;
}
.dragging {
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.1);
background-color: #f8f8f8;
}
</style>
下拉刷新与上拉加载
<template>
<view class="container">
<!-- 自定义下拉刷新 -->
<view
class="refresh-container"
:style="{ height: refreshHeight + 'px' }"
:class="{ 'refreshing': isRefreshing }"
>
<view class="refresh-icon" :class="{ 'rotate': isRefreshing }">↓</view>
<text>{{ refreshText }}</text>
</view>
<!-- 内容区域 -->
<scroll-view
scroll-y
class="scroll-view"
@scrolltoupper="handleScrollToUpper"
@scrolltolower="handleScrollToLower"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
:style="{ transform: `translateY(${translateY}px)` }"
>
<view class="list">
<view
v-for="item in list"
:key="item.id"
class="list-item"
>
<text class="item-title">{{ item.title }}</text>
<text class="item-desc">{{ item.description }}</text>
</view>
</view>
<!-- 加载更多 -->
<view class="loading-more" v-if="hasMore || isLoadingMore">
<view class="loading-icon" v-if="isLoadingMore"></view>
<text>{{ loadingMoreText }}</text>
</view>
</scroll-view>
</view>
</template>
<script>
export default {
data() {
return {
list: [],
page: 1,
pageSize: 10,
hasMore: true,
isLoadingMore: false,
isRefreshing: false,
// 下拉刷新相关
startY: 0,
moveY: 0,
translateY: 0,
refreshHeight: 0,
maxRefreshHeight: 80,
refreshThreshold: 50,
isTouching: false
}
},
computed: {
refreshText() {
if (this.isRefreshing) {
return '刷新中...'
}
return this.refreshHeight >= this.refreshThreshold ? '释放立即刷新' : '下拉可以刷新'
},
loadingMoreText() {
if (!this.hasMore) {
return '没有更多数据了'
}
return this.isLoadingMore ? '正在加载更多...' : '上拉加载更多'
}
},
created() {
// 初始加载数据
this.loadData()
},
methods: {
// 加载数据
loadData(isRefresh = false) {
if (isRefresh) {
this.page = 1
this.hasMore = true
}
// 模拟请求
setTimeout(() => {
const newData = Array.from({ length: this.pageSize }, (_, index) => {
const itemIndex = (this.page - 1) * this.pageSize + index + 1
return {
id: `item-${this.page}-${index}`,
title: `标题 ${itemIndex}`,
description: `这是第${itemIndex}条数据的详细描述信息,包含了一些相关内容。`
}
})
if (isRefresh) {
this.list = newData
} else {
this.list = [...this.list, ...newData]
}
// 判断是否还有更多数据
if (this.page >= 5) {
this.hasMore = false
} else {
this.page++
}
// 重置状态
this.isRefreshing = false
this.isLoadingMore = false
// 如果是刷新,需要重置位置
if (isRefresh) {
this.resetRefresh()
}
}, 1000)
},
// 下拉刷新相关方法
handleTouchStart(e) {
// 只有在顶部才允许下拉刷新
if (e.touches[0] && !this.isRefreshing) {
this.startY = e.touches[0].clientY
this.isTouching = true
}
},
handleTouchMove(e) {
if (!this.isTouching || this.isRefreshing) return
this.moveY = e.touches[0].clientY
let distance = this.moveY - this.startY
// 只有下拉才触发刷新
if (distance <= 0) {
this.translateY = 0
this.refreshHeight = 0
return
}
// 添加阻尼效果
distance = Math.pow(distance, 0.8)
// 限制最大下拉距离
if (distance > this.maxRefreshHeight) {
distance = this.maxRefreshHeight
}
this.translateY = distance
this.refreshHeight = distance
},
handleTouchEnd() {
if (!this.isTouching || this.isRefreshing) return
this.isTouching = false
// 如果达到刷新阈值,触发刷新
if (this.refreshHeight >= this.refreshThreshold) {
this.isRefreshing = true
this.translateY = this.refreshThreshold
this.refreshHeight = this.refreshThreshold
// 执行刷新
this.loadData(true)
} else {
// 未达到阈值,重置位置
this.resetRefresh()
}
},
resetRefresh() {
this.translateY = 0
this.refreshHeight = 0
},
// 滚动事件处理
handleScrollToUpper() {
console.log('到达顶部')
},
handleScrollToLower() {
if (this.hasMore && !this.isLoadingMore) {
console.log('到达底部,加载更多')
this.isLoadingMore = true
this.loadData()
}
}
}
}
</script>
<style>
.container {
height: 100vh;
position: relative;
overflow: hidden;
}
.refresh-container {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 0;
transition: height 0.2s;
overflow: hidden;
background-color: #f5f5f5;
z-index: 1;
}
.refresh-icon {
font-size: 32rpx;
margin-right: 10rpx;
transition: transform 0.3s;
}
.rotate {
animation: rotating 1s linear infinite;
}
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.scroll-view {
height: 100%;
transition: transform 0.2s;
}
.list {
padding: 20rpx;
}
.list-item {
background-color: #ffffff;
border-radius: 8rpx;
padding: 20rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
}
.item-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 10rpx;
display: block;
}
.item-desc {
font-size: 28rpx;
color: #666;
display: block;
}
.loading-more {
text-align: center;
padding: 20rpx 0;
color: #999;
font-size: 24rpx;
display: flex;
justify-content: center;
align-items: center;
}
.loading-icon {
width: 30rpx;
height: 30rpx;
border: 2rpx solid #ccc;
border-top-color: #666;
border-radius: 50%;
margin-right: 10rpx;
animation: rotating 1s linear infinite;
}
</style>
图片预览与手势缩放
<template>
<view class="container">
<!-- 图片列表 -->
<view class="image-grid">
<view
v-for="(image, index) in images"
:key="index"
class="image-item"
@tap="previewImage(index)"
>
<image :src="image.thumbnail" mode="aspectFill" class="thumbnail"></image>
</view>
</view>
<!-- 图片预览层 -->
<view
class="preview-container"
v-if="showPreview"
@tap="closePreview"
>
<swiper
class="preview-swiper"
:current="currentIndex"
@change="handleSwiperChange"
circular
>
<swiper-item
v-for="(image, index) in images"
:key="index"
class="preview-item"
>
<view
class="zoom-container"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
<image
:src="image.original"
mode="aspectFit"
class="preview-image"
:style="getZoomStyle(index)"
></image>
</view>
</swiper-item>
</swiper>
<!-- 指示器 -->
<view class="indicator">
{{ currentIndex + 1 }}/{{ images.length }}
</view>
<!-- 关闭按钮 -->
<view class="close-btn" @tap.stop="closePreview">×</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
images: [
{
thumbnail: '/static/images/thumb1.jpg',
original: '/static/images/image1.jpg'
},
{
thumbnail: '/static/images/thumb2.jpg',
original: '/static/images/image2.jpg'
},
{
thumbnail: '/static/images/thumb3.jpg',
original: '/static/images/image3.jpg'
},
{
thumbnail: '/static/images/thumb4.jpg',
original: '/static/images/image4.jpg'
},
{
thumbnail: '/static/images/thumb5.jpg',
original: '/static/images/image5.jpg'
},
{
thumbnail: '/static/images/thumb6.jpg',
original: '/static/images/image6.jpg'
}
],
showPreview: false,
currentIndex: 0,
// 缩放相关
scale: 1,
baseScale: 1,
lastScale: 1,
offsetX: 0,
offsetY: 0,
lastX: 0,
lastY: 0,
touches: []
}
},
methods: {
previewImage(index) {
this.currentIndex = index
this.showPreview = true
this.resetZoom()
},
closePreview() {
this.showPreview = false
},
handleSwiperChange(e) {
this.currentIndex = e.detail.current
this.resetZoom()
},
// 手势缩放相关
handleTouchStart(e) {
const touches = e.touches
this.touches = touches
if (touches.length === 1) {
// 单指拖动
this.lastX = touches[0].clientX
this.lastY = touches[0].clientY
} else if (touches.length === 2) {
// 双指缩放
const touch1 = touches[0]
const touch2 = touches[1]
// 计算两指之间的距离
const distance = this.getDistance(
touch1.clientX, touch1.clientY,
touch2.clientX, touch2.clientY
)
this.baseScale = distance
}
// 阻止事件冒泡,防止关闭预览
e.stopPropagation()
},
handleTouchMove(e) {
const touches = e.touches
if (touches.length === 1 && this.scale > 1) {
// 单指拖动(仅当放大时可拖动)
const touch = touches[0]
const deltaX = touch.clientX - this.lastX
const deltaY = touch.clientY - this.lastY
this.offsetX += deltaX
this.offsetY += deltaY
this.lastX = touch.clientX
this.lastY = touch.clientY
} else if (touches.length === 2) {
// 双指缩放
const touch1 = touches[0]
const touch2 = touches[1]
// 计算新的两指距离
const distance = this.getDistance(
touch1.clientX, touch1.clientY,
touch2.clientX, touch2.clientY
)
// 计算缩放比例
let newScale = (distance / this.baseScale) * this.lastScale
// 限制缩放范围
if (newScale < 1) newScale = 1
if (newScale > 3) newScale = 3
this.scale = newScale
}
// 阻止默认行为和冒泡
e.preventDefault()
e.stopPropagation()
},
handleTouchEnd(e) {
if (this.touches.length === 2) {
this.lastScale = this.scale
}
// 如果缩小到原始大小,重置偏移
if (this.scale <= 1) {
this.resetZoom()
}
// 阻止事件冒泡
e.stopPropagation()
},
getDistance(x1, y1, x2, y2) {
const deltaX = x2 - x1
const deltaY = y2 - y1
return Math.sqrt(deltaX * deltaX + deltaY * deltaY)
},
resetZoom() {
this.scale = 1
this.lastScale = 1
this.offsetX = 0
this.offsetY = 0
},
getZoomStyle(index) {
if (index !== this.currentIndex) {
return {}
}
return {
transform: `scale(${this.scale}) translate(${this.offsetX / this.scale}px, ${this.offsetY / this.scale}px)`
}
}
}
}
</script>
<style>
.container {
padding: 20rpx;
}
.image-grid {
display: flex;
flex-wrap: wrap;
}
.image-item {
width: 33.33%;
padding: 10rpx;
box-sizing: border-box;
}
.thumbnail {
width: 100%;
height: 200rpx;
border-radius: 8rpx;
}
.preview-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
z-index: 999;
display: flex;
justify-content: center;
align-items: center;
}
.preview-swiper {
width: 100%;
height: 100%;
}
.preview-item {
display: flex;
justify-content: center;
align-items: center;
}
.zoom-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.preview-image {
max-width: 100%;
max-height: 100%;
transition: transform 0.1s ease;
}
.indicator {
position: absolute;
bottom: 60rpx;
left: 0;
right: 0;
text-align: center;
color: #fff;
font-size: 28rpx;
}
.close-btn {
position: absolute;
top: 40rpx;
right: 40rpx;
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 40rpx;
display: flex;
justify-content: center;
align-items: center;
}
</style>
总结
事件处理是uni-app开发中不可或缺的一部分,掌握好事件处理机制可以帮助开发者构建更加交互丰富、用户体验更好的应用。本文介绍了uni-app中的事件绑定语法、常用事件类型、事件修饰符、事件对象、自定义事件、手势识别等内容,并提供了多个实际应用示例。
在实际开发中,应注意以下几点:
选择合适的事件类型:根据交互需求选择合适的事件类型,如点击使用
tap
,长按使用longpress
等。注意跨平台差异:不同平台(小程序、H5、App)的事件处理可能存在差异,编写代码时应考虑兼容性。
优化性能:对于频繁触发的事件,使用防抖或节流技术进行优化;避免在模板中使用内联函数;合理使用计算属性代替方法。
合理组织代码:将复杂的事件处理逻辑拆分为多个方法,提高代码可读性和可维护性。
通过合理使用事件处理机制,可以构建出交互流畅、体验良好的uni-app应用。