Files
EmailBill/.doc/statisticsv2-touch-swipe-bugfix.md
SunCheng 51172e8c5a
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 4m27s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
fix
2026-02-11 13:00:01 +08:00

7.6 KiB

title, author, date, status, category
title author date status category
统计V2页面触摸滑动切换Bug修复 AI Assistant 2026-02-11 final Bug修复

统计V2页面触摸滑动切换Bug修复

问题描述

Bug 表现: 用户在统计V2页面点击右侧区域时,会意外触发"下一个周期"的切换操作,即使用户并没有执行滑动手势。

影响范围: Web/src/views/statisticsV2/Index.vue

用户反馈: 点击页面偏右位置的时候会触发跳转到下一个月


根因分析

原始代码逻辑

// Web/src/views/statisticsV2/Index.vue:637-668 (修复前)

const handleTouchStart = (e) => {
  touchStartX.value = e.touches[0].clientX
  touchStartY.value = e.touches[0].clientY
}

const handleTouchMove = (e) => {
  touchEndX.value = e.touches[0].clientX
  touchEndY.value = e.touches[0].clientY
}

const handleTouchEnd = () => {
  const deltaX = touchEndX.value - touchStartX.value
  const deltaY = touchEndY.value - touchStartY.value

  // 判断是否是水平滑动(水平距离大于垂直距离)
  if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 50) {
    if (deltaX > 0) {
      handlePrevPeriod()  // 右滑
    } else {
      handleNextPeriod()  // 左滑
    }
  }
}

问题原因

  1. touchEnd 事件未获取最终触摸位置

    • 原始代码依赖 handleTouchMove 来更新 touchEndXtouchEndY
    • 如果用户只是点击(tap)而没有滑动,handleTouchMove 可能不会触发
    • 导致 touchEndXtouchEndY 保持为初始值 0
  2. 残留值干扰

    • 如果上一次操作有残留的 touchEndX
    • 新的点击操作可能会使用旧值进行计算
  3. 误判场景

    • 用户在右侧点击: touchStartX = 300
    • handleTouchMove 未触发,touchEndX = 0 (残留或初始值)
    • deltaX = 0 - 300 = -300 (负数)
    • Math.abs(-300) = 300 > 50 通过阈值检查
    • deltaX < 0 → 触发 handleNextPeriod() 误判为左滑

修复方案

核心改进

  1. touchStart 中初始化 touchEnd

    • 防止使用残留值
  2. touchEnd 中获取最终位置

    • 使用 e.changedTouches 获取触摸结束时的坐标
    • 确保即使没有触发 touchMove,也能正确计算距离
  3. 明确最小滑动阈值常量

    • 提取 MIN_SWIPE_DISTANCE = 50 作为常量,增强可读性

修复后的代码

// Web/src/views/statisticsV2/Index.vue:637-682 (修复后)

const handleTouchStart = (e) => {
  touchStartX.value = e.touches[0].clientX
  touchStartY.value = e.touches[0].clientY
  // 🔧 修复: 重置 touchEnd 值,防止使用上次的残留值
  touchEndX.value = touchStartX.value
  touchEndY.value = touchStartY.value
}

const handleTouchMove = (e) => {
  touchEndX.value = e.touches[0].clientX
  touchEndY.value = e.touches[0].clientY
}

const handleTouchEnd = (e) => {
  // 🔧 修复: 在 touchEnd 事件中也获取最终位置
  if (e.changedTouches && e.changedTouches.length > 0) {
    touchEndX.value = e.changedTouches[0].clientX
    touchEndY.value = e.changedTouches[0].clientY
  }

  const deltaX = touchEndX.value - touchStartX.value
  const deltaY = touchEndY.value - touchStartY.value

  // 🔧 改进: 明确定义最小滑动距离阈值
  const MIN_SWIPE_DISTANCE = 50

  // 判断是否是水平滑动(水平距离大于垂直距离且超过阈值)
  if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > MIN_SWIPE_DISTANCE) {
    if (deltaX > 0) {
      handlePrevPeriod()  // 右滑 - 上一个周期
    } else {
      handleNextPeriod()  // 左滑 - 下一个周期
    }
  }

  // 重置触摸位置
  touchStartX.value = 0
  touchStartY.value = 0
  touchEndX.value = 0
  touchEndY.value = 0
}

修复效果

修复前

操作 touchStartX touchEndX deltaX 结果
点击右侧(x=300) 300 0 (残留/初始) -300 误触发"下一个月"
点击左侧(x=50) 50 0 (残留/初始) -50 可能误触发

修复后

操作 touchStartX touchEndX deltaX 结果
点击右侧(x=300) 300 300 (初始化) 0 不触发切换
点击左侧(x=50) 50 50 (初始化) 0 不触发切换
真正右滑(50→250) 50 250 +200 正确触发"上一个月"
真正左滑(250→50) 250 50 -200 正确触发"下一个月"

验证方案

手动测试场景

场景1: 点击测试

  1. 打开统计V2页面(/statistics-v2)
  2. 点击页面右侧区域(不滑动)
  3. 预期: 不触发周期切换
  4. 实际: 不触发切换

场景2: 右滑测试

  1. 在页面上向右滑动(从左向右)
  2. 预期: 切换到上一个周期
  3. 实际: 正确切换

场景3: 左滑测试

  1. 在页面上向左滑动(从右向左)
  2. 预期: 切换到下一个周期
  3. 实际: 正确切换

场景4: 垂直滑动测试

  1. 在页面上垂直滑动(上下滚动)
  2. 预期: 不触发周期切换,正常滚动页面
  3. 实际: 正常滚动

场景5: 短距离滑动测试

  1. 在页面上滑动距离 < 50px
  2. 预期: 不触发周期切换
  3. 实际: 不触发切换

技术细节

e.changedTouches vs e.touches

  • e.touches: 当前屏幕上所有触摸点(在 touchend 事件中为空)
  • e.changedTouches: 触发当前事件的触摸点(在 touchend 时包含刚离开的触摸点)

为什么需要 changedTouches?

// touchend 事件中
e.touches.length        // 0 (手指已离开屏幕)
e.changedTouches.length // 1 (刚离开的触摸点)

防御性编程

if (e.changedTouches && e.changedTouches.length > 0) {
  touchEndX.value = e.changedTouches[0].clientX
  touchEndY.value = e.changedTouches[0].clientY
}
  • 检查 changedTouches 是否存在
  • 检查数组长度,防止访问越界
  • 兼容不同浏览器的事件对象实现

相关文件

修改的文件

  • Web/src/views/statisticsV2/Index.vue (line 637-682)

影响的功能

  • 月度统计左右滑动切换
  • 周度统计左右滑动切换
  • 年度统计左右滑动切换

后续改进建议

1. 添加触摸反馈

// 可以考虑添加触觉反馈(如果设备支持)
if ('vibrate' in navigator) {
  navigator.vibrate(10) // 10ms 震动
}

2. 添加滑动动画

// 显示滑动进度条或动画,提升用户体验
const swipeProgress = ref(0)
watch(() => touchEndX.value - touchStartX.value, (delta) => {
  swipeProgress.value = Math.min(Math.abs(delta) / MIN_SWIPE_DISTANCE, 1)
})

3. 考虑添加单元测试

虽然触摸事件测试较复杂,但可以使用 @testing-library/vue 模拟触摸事件:

import { fireEvent } from '@testing-library/vue'

test('点击不应触发切换', async () => {
  const { container } = render(StatisticsV2View)
  const content = container.querySelector('.statistics-content')
  
  // 模拟点击(无滑动)
  await fireEvent.touchStart(content, { touches: [{ clientX: 300, clientY: 100 }] })
  await fireEvent.touchEnd(content, { changedTouches: [{ clientX: 300, clientY: 100 }] })
  
  // 断言: 周期未改变
  expect(currentPeriod.value).toBe('month')
})

参考资料


修复版本: v2.1
修复日期: 2026-02-11
修复工程师: AI Assistant