Skip to content

事件处理

在uni-app中,事件处理是实现用户交互的重要机制。本文将详细介绍uni-app中的事件处理方式、事件类型以及最佳实践。

事件绑定

基本语法

uni-app沿用了Vue的事件处理语法,使用v-on指令(简写为@)来监听DOM事件:

html
<!-- 完整语法 -->
<button v-on:tap="handleTap">点击我</button>

<!-- 缩写语法 -->
<button @tap="handleTap">点击我</button>

事件处理方法定义在组件的methods选项中:

javascript
export default {
  methods: {
    handleTap() {
      console.log('按钮被点击了')
    }
  }
}

内联事件处理

对于简单的事件处理逻辑,可以直接在模板中使用内联JavaScript语句:

html
<button @tap="counter += 1">计数器: {{ counter }}</button>

事件传参

在事件处理方法中,可以传入自定义参数:

html
<button @tap="handleTap('hello', $event)">带参数的事件</button>
javascript
methods: {
  handleTap(message, event) {
    console.log(message) // 'hello'
    console.log(event) // 原生事件对象
  }
}

提示

使用$event可以在传入自定义参数的同时,获取到原生的事件对象。

常用事件类型

uni-app支持多种事件类型,以下是一些常用的事件:

触摸事件

  • tap:点击事件,类似于HTML中的click事件
  • longpress:长按事件,手指长时间触摸
  • touchstart:触摸开始事件
  • touchmove:触摸移动事件
  • touchend:触摸结束事件
  • touchcancel:触摸取消事件
html
<view @tap="handleTap">点击</view>
<view @longpress="handleLongPress">长按</view>
<view 
  @touchstart="handleTouchStart" 
  @touchmove="handleTouchMove"
  @touchend="handleTouchEnd"
>
  触摸区域
</view>

表单事件

  • input:输入框内容变化时触发
  • focus:输入框聚焦时触发
  • blur:输入框失去焦点时触发
  • change:选择器、开关等值变化时触发
  • submit:表单提交时触发
html
<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:页面隐藏时触发

这些事件通常在页面的生命周期钩子函数中处理:

javascript
export default {
  onLoad(options) {
    console.log('页面加载', options)
  },
  onReady() {
    console.log('页面初次渲染完成')
  },
  onShow() {
    console.log('页面显示')
  },
  onHide() {
    console.log('页面隐藏')
  }
}

滚动事件

  • scroll:滚动时触发
html
<scroll-view 
  scroll-y 
  @scroll="handleScroll" 
  style="height: 300px;"
>
  <view v-for="item in items" :key="item.id">
    {{ item.text }}
  </view>
</scroll-view>
javascript
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:事件只触发一次
html
<!-- 阻止事件冒泡 -->
<view @tap.stop="handleTap">阻止冒泡</view>

<!-- 阻止默认行为 -->
<form @submit.prevent="handleSubmit">
  <!-- 表单内容 -->
</form>

<!-- 只触发一次 -->
<button @tap.once="handleTap">只能点击一次</button>

按键修饰符

在处理键盘事件时,可以使用按键修饰符:

html
<!-- 按下回车键时提交表单 -->
<input @keyup.enter="submit" />

<!-- 按下Esc键时取消操作 -->
<input @keyup.esc="cancel" />

注意

按键修饰符主要在H5平台有效,在小程序和App平台可能需要使用其他方式处理键盘事件。

事件对象

事件处理函数会自动接收一个事件对象(event),包含事件的相关信息:

html
<view @tap="handleTap">点击获取事件信息</view>
javascript
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)的事件对象可能存在差异,建议使用以下方式获取通用属性:

javascript
methods: {
  handleInput(event) {
    // 获取输入值
    const value = event.detail.value || event.target.value
    
    // 获取dataset
    const dataset = event.currentTarget.dataset
  }
}

自定义事件

组件间通信

在自定义组件中,可以使用$emit方法触发自定义事件,实现子组件向父组件通信:

子组件(child.vue):

html
<template>
  <view>
    <button @tap="sendMessage">发送消息</button>
  </view>
</template>

<script>
export default {
  methods: {
    sendMessage() {
      // 触发自定义事件,并传递数据
      this.$emit('message', {
        content: '这是来自子组件的消息',
        time: new Date()
      })
    }
  }
}
</script>

父组件:

html
<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)

对于非父子组件间的通信,可以使用事件总线:

javascript
// eventBus.js
import Vue from 'vue'
export const eventBus = new Vue()

// 在Vue 3中可以使用mitt库
// import mitt from 'mitt'
// export const eventBus = mitt()

组件A(发送事件):

javascript
import { eventBus } from '@/utils/eventBus'

export default {
  methods: {
    sendGlobalMessage() {
      eventBus.$emit('global-message', {
        from: 'ComponentA',
        content: '全局消息'
      })
    }
  }
}

组件B(接收事件):

javascript
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提供了基础的触摸事件,但对于复杂的手势识别(如滑动、捏合、旋转等),可以使用以下方法:

自定义手势识别

html
<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平台):

bash
# 安装hammerjs
npm install hammerjs
html
<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)是一种常用的事件处理模式,通过将事件监听器添加到父元素,而不是每个子元素,可以提高性能并简化代码:

html
<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)技术来优化性能:

javascript
// 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)
    }
  }
}

在组件中使用:

html
<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>

避免内联函数

在模板中使用内联函数会导致每次重新渲染时创建新的函数实例,应尽量避免:

html
<!-- 不推荐 -->
<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>
javascript
methods: {
  handleItemClick(e) {
    const id = e.currentTarget.dataset.id
    // 处理点击逻辑
  }
}

使用计算属性代替方法

对于在模板中多次使用的数据转换,应使用计算属性而不是方法:

html
<!-- 不推荐 -->
<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>
javascript
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推荐的事件名:

html
<!-- 在所有平台都使用tap事件 -->
<view @tap="handleTap">点击</view>

<!-- 在所有平台都使用change事件 -->
<picker @change="handleChange" :range="options">选择</picker>

事件对象差异

不同平台的事件对象结构可能不同,例如:

  • Web平台通过event.target.value获取输入值
  • 小程序通过event.detail.value获取输入值

为了处理这些差异,可以编写兼容性代码:

javascript
methods: {
  handleInput(event) {
    // 兼容不同平台
    const value = event.detail.value || (event.target && event.target.value) || ''
    this.inputValue = value
  }
}

事件冒泡差异

不同平台的事件冒泡机制可能存在差异,特别是在自定义组件嵌套时:

html
<!-- 父组件 -->
<view @tap="handleOuterTap">
  <custom-component @tap="handleInnerTap"></custom-component>
</view>

在某些平台上,点击自定义组件可能会同时触发内部和外部的tap事件。为了确保一致的行为,可以使用.stop修饰符:

html
<view @tap="handleOuterTap">
  <custom-component @tap.stop="handleInnerTap"></custom-component>
</view>

实际应用示例

拖拽排序列表

html
<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>

下拉刷新与上拉加载

html
<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>

图片预览与手势缩放

html
<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中的事件绑定语法、常用事件类型、事件修饰符、事件对象、自定义事件、手势识别等内容,并提供了多个实际应用示例。

在实际开发中,应注意以下几点:

  1. 选择合适的事件类型:根据交互需求选择合适的事件类型,如点击使用tap,长按使用longpress等。

  2. 注意跨平台差异:不同平台(小程序、H5、App)的事件处理可能存在差异,编写代码时应考虑兼容性。

  3. 优化性能:对于频繁触发的事件,使用防抖或节流技术进行优化;避免在模板中使用内联函数;合理使用计算属性代替方法。

  4. 合理组织代码:将复杂的事件处理逻辑拆分为多个方法,提高代码可读性和可维护性。

通过合理使用事件处理机制,可以构建出交互流畅、体验良好的uni-app应用。

一次开发,多端部署 - 让跨平台开发更简单