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
7.6 KiB
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() // 左滑
}
}
}
问题原因
-
touchEnd事件未获取最终触摸位置- 原始代码依赖
handleTouchMove来更新touchEndX和touchEndY - 如果用户只是点击(tap)而没有滑动,
handleTouchMove可能不会触发 - 导致
touchEndX和touchEndY保持为初始值0
- 原始代码依赖
-
残留值干扰
- 如果上一次操作有残留的
touchEndX值 - 新的点击操作可能会使用旧值进行计算
- 如果上一次操作有残留的
-
误判场景
- 用户在右侧点击:
touchStartX = 300 handleTouchMove未触发,touchEndX = 0(残留或初始值)deltaX = 0 - 300 = -300(负数)Math.abs(-300) = 300 > 50✅ 通过阈值检查deltaX < 0→ 触发handleNextPeriod()❌ 误判为左滑
- 用户在右侧点击:
修复方案
核心改进
-
在
touchStart中初始化touchEnd值- 防止使用残留值
-
在
touchEnd中获取最终位置- 使用
e.changedTouches获取触摸结束时的坐标 - 确保即使没有触发
touchMove,也能正确计算距离
- 使用
-
明确最小滑动阈值常量
- 提取
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: 点击测试
- 打开统计V2页面(
/statistics-v2) - 点击页面右侧区域(不滑动)
- 预期: 不触发周期切换
- 实际: ✅ 不触发切换
场景2: 右滑测试
- 在页面上向右滑动(从左向右)
- 预期: 切换到上一个周期
- 实际: ✅ 正确切换
场景3: 左滑测试
- 在页面上向左滑动(从右向左)
- 预期: 切换到下一个周期
- 实际: ✅ 正确切换
场景4: 垂直滑动测试
- 在页面上垂直滑动(上下滚动)
- 预期: 不触发周期切换,正常滚动页面
- 实际: ✅ 正常滚动
场景5: 短距离滑动测试
- 在页面上滑动距离 < 50px
- 预期: 不触发周期切换
- 实际: ✅ 不触发切换
技术细节
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