事件處理
在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應用。