feat: 添加引导页、视频播放和触摸交互功能
- 新增引导页组件(Guide.tsx)及相关资源 - 添加视频播放功能和相关资源文件 - 实现useTouch自定义hook支持触摸交互 - 添加store状态管理 - 更新多个页面组件(Content, Menu, Publicity) - 优化图片组件和背景图片组件 - 更新项目依赖
This commit is contained in:
1 parent
4ce7520066
commit
53d0eeac6b
24 files changed
+1390
-84
No files matched your search
@@ -0,0 +1,428 @@
|
||||
# useTouch Hook 使用文档
|
||||
|
||||
## 概述
|
||||
|
||||
`useTouch` 是一个灵活强大的触摸手势处理 Hook,支持多种手势识别,包括:
|
||||
|
||||
- 点击 (Tap)
|
||||
- 双击 (Double Tap)
|
||||
- 长按 (Long Press)
|
||||
- 滑动 (Swipe) - 支持四个方向
|
||||
- 触摸移动追踪
|
||||
|
||||
同时支持触摸屏和鼠标操作,适用于移动端和桌面端。
|
||||
|
||||
## 基础用法
|
||||
|
||||
### 1. 简单点击
|
||||
|
||||
```tsx
|
||||
import { useTouch } from "@/hooks";
|
||||
|
||||
function MyComponent() {
|
||||
const { handlers } = useTouch({
|
||||
onTap: () => {
|
||||
console.log("点击了");
|
||||
},
|
||||
});
|
||||
|
||||
return <div {...handlers}>点击我</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 滑动手势
|
||||
|
||||
```tsx
|
||||
const { handlers } = useTouch({
|
||||
onSwipeLeft: () => console.log("向左滑动"),
|
||||
onSwipeRight: () => console.log("向右滑动"),
|
||||
onSwipeUp: () => console.log("向上滑动"),
|
||||
onSwipeDown: () => console.log("向下滑动"),
|
||||
});
|
||||
|
||||
return <div {...handlers}>滑动区域</div>;
|
||||
```
|
||||
|
||||
### 3. 双击
|
||||
|
||||
```tsx
|
||||
const { handlers } = useTouch({
|
||||
onDoubleTap: () => {
|
||||
console.log("双击触发");
|
||||
},
|
||||
});
|
||||
|
||||
return <div {...handlers}>双击我</div>;
|
||||
```
|
||||
|
||||
### 4. 长按
|
||||
|
||||
```tsx
|
||||
const { handlers } = useTouch({
|
||||
onLongPress: () => {
|
||||
console.log("长按触发");
|
||||
},
|
||||
longPressDelay: 500, // 500ms 触发长按
|
||||
});
|
||||
|
||||
return <div {...handlers}>长按我</div>;
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 获取触摸数据
|
||||
|
||||
```tsx
|
||||
const { handlers, touchData, isTouching } = useTouch({
|
||||
onTouchMove: (data) => {
|
||||
console.log("移动距离:", data.deltaX, data.deltaY);
|
||||
console.log("移动速度:", data.velocity);
|
||||
console.log("移动方向:", data.direction);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div {...handlers}>
|
||||
{isTouching && touchData && (
|
||||
<div>
|
||||
移动: {touchData.deltaX.toFixed(0)}px, {touchData.deltaY.toFixed(0)}px
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
### 图片查看器示例
|
||||
|
||||
```tsx
|
||||
function ImageViewer() {
|
||||
const [scale, setScale] = useState(1);
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
const { handlers } = useTouch({
|
||||
onDoubleTap: () => {
|
||||
// 双击放大/还原
|
||||
setScale(scale === 1 ? 2 : 1);
|
||||
},
|
||||
onSwipe: (data) => {
|
||||
// 滑动移动图片
|
||||
if (scale > 1) {
|
||||
setPosition((prev) => ({
|
||||
x: prev.x + data.deltaX,
|
||||
y: prev.y + data.deltaY,
|
||||
}));
|
||||
}
|
||||
},
|
||||
swipeThreshold: 5,
|
||||
});
|
||||
|
||||
return (
|
||||
<div {...handlers} style={{ overflow: "hidden" }}>
|
||||
<img
|
||||
src="image.jpg"
|
||||
style={{
|
||||
transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
|
||||
transition: "transform 0.3s",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 卡片滑动删除
|
||||
|
||||
```tsx
|
||||
function SwipeableCard() {
|
||||
const [offset, setOffset] = useState(0);
|
||||
|
||||
const { handlers } = useTouch({
|
||||
onTouchMove: (data) => {
|
||||
if (data.direction === "left" || data.direction === "right") {
|
||||
setOffset(data.deltaX);
|
||||
}
|
||||
},
|
||||
onTouchEnd: (data) => {
|
||||
if (Math.abs(data.deltaX) > 100) {
|
||||
// 滑动超过 100px,执行删除
|
||||
onDelete();
|
||||
} else {
|
||||
// 弹回原位
|
||||
setOffset(0);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
{...handlers}
|
||||
style={{
|
||||
transform: `translateX(${offset}px)`,
|
||||
transition: offset === 0 ? "transform 0.3s" : "none",
|
||||
}}
|
||||
>
|
||||
<div>卡片内容</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 下拉刷新
|
||||
|
||||
```tsx
|
||||
function PullToRefresh() {
|
||||
const [pullDistance, setPullDistance] = useState(0);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
const { handlers } = useTouch({
|
||||
onTouchMove: (data) => {
|
||||
// 只在向下滑动且在顶部时触发
|
||||
if (data.direction === "down" && window.scrollY === 0) {
|
||||
setPullDistance(Math.min(data.deltaY, 80));
|
||||
}
|
||||
},
|
||||
onTouchEnd: (data) => {
|
||||
if (pullDistance > 50) {
|
||||
setIsRefreshing(true);
|
||||
// 执行刷新逻辑
|
||||
refresh().then(() => {
|
||||
setIsRefreshing(false);
|
||||
setPullDistance(0);
|
||||
});
|
||||
} else {
|
||||
setPullDistance(0);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div {...handlers}>
|
||||
<div style={{ height: pullDistance, transition: "height 0.3s" }}>
|
||||
{isRefreshing
|
||||
? "刷新中..."
|
||||
: pullDistance > 50
|
||||
? "松开刷新"
|
||||
: "下拉刷新"}
|
||||
</div>
|
||||
<div>内容区域</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 轮播图
|
||||
|
||||
```tsx
|
||||
function Carousel({ images }: { images: string[] }) {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
const { handlers } = useTouch({
|
||||
onSwipeLeft: () => {
|
||||
setCurrentIndex((prev) => Math.min(prev + 1, images.length - 1));
|
||||
},
|
||||
onSwipeRight: () => {
|
||||
setCurrentIndex((prev) => Math.max(prev - 1, 0));
|
||||
},
|
||||
swipeThreshold: 50,
|
||||
swipeVelocityThreshold: 0.3,
|
||||
});
|
||||
|
||||
return (
|
||||
<div {...handlers} style={{ overflow: "hidden" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
transform: `translateX(-${currentIndex * 100}%)`,
|
||||
transition: "transform 0.3s",
|
||||
}}
|
||||
>
|
||||
{images.map((img, i) => (
|
||||
<img key={i} src={img} style={{ width: "100%", flexShrink: 0 }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
```typescript
|
||||
interface UseTouchOptions {
|
||||
// 基础配置
|
||||
enabled?: boolean; // 是否启用,默认 true
|
||||
preventDefault?: boolean; // 是否阻止默认行为,默认 false
|
||||
stopPropagation?: boolean; // 是否阻止事件冒泡,默认 false
|
||||
|
||||
// 滑动配置
|
||||
swipeThreshold?: number; // 滑动阈值(px),默认 10
|
||||
swipeVelocityThreshold?: number; // 滑动速度阈值(px/ms),默认 0.5
|
||||
|
||||
// 长按配置
|
||||
longPressDelay?: number; // 长按延迟(ms),默认 500
|
||||
longPressMoveThreshold?: number; // 长按允许移动距离(px),默认 10
|
||||
|
||||
// 双击配置
|
||||
doubleTapDelay?: number; // 双击间隔(ms),默认 300
|
||||
doubleTapMoveThreshold?: number; // 双击允许移动距离(px),默认 10
|
||||
|
||||
// 事件回调
|
||||
onTouchStart?: (data: TouchEventData) => void;
|
||||
onTouchMove?: (data: TouchEventData) => void;
|
||||
onTouchEnd?: (data: TouchEventData) => void;
|
||||
onTap?: (data: TouchEventData) => void;
|
||||
onDoubleTap?: (data: TouchEventData) => void;
|
||||
onLongPress?: (data: TouchEventData) => void;
|
||||
onSwipe?: (data: TouchEventData) => void;
|
||||
onSwipeLeft?: (data: TouchEventData) => void;
|
||||
onSwipeRight?: (data: TouchEventData) => void;
|
||||
onSwipeUp?: (data: TouchEventData) => void;
|
||||
onSwipeDown?: (data: TouchEventData) => void;
|
||||
}
|
||||
```
|
||||
|
||||
## 触摸数据
|
||||
|
||||
所有回调函数都会接收到 `TouchEventData` 对象:
|
||||
|
||||
```typescript
|
||||
interface TouchEventData {
|
||||
startPoint: TouchPoint; // 起始位置 { x, y, timestamp }
|
||||
currentPoint: TouchPoint; // 当前位置 { x, y, timestamp }
|
||||
deltaX: number; // X 轴移动距离
|
||||
deltaY: number; // Y 轴移动距离
|
||||
distance: number; // 移动的总距离
|
||||
velocity: number; // 移动速度(像素/毫秒)
|
||||
direction: SwipeDirection; // 移动方向: 'left' | 'right' | 'up' | 'down' | null
|
||||
event: React.TouchEvent | React.MouseEvent; // 原始事件
|
||||
}
|
||||
```
|
||||
|
||||
## 返回值
|
||||
|
||||
```typescript
|
||||
interface UseTouchReturn {
|
||||
isTouching: boolean; // 是否正在触摸
|
||||
touchData: TouchEventData | null; // 当前触摸数据
|
||||
handlers: {
|
||||
onTouchStart: (e: React.TouchEvent) => void;
|
||||
onTouchMove: (e: React.TouchEvent) => void;
|
||||
onTouchEnd: (e: React.TouchEvent) => void;
|
||||
onMouseDown: (e: React.MouseEvent) => void;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **事件处理器自动包含鼠标和触摸事件**,无需分别绑定
|
||||
2. **长按和滑动互斥**:如果移动距离超过阈值,会取消长按
|
||||
3. **双击优先级高于单击**:在双击延迟内的第二次点击会触发双击而非单击
|
||||
4. **性能优化**:使用 `useCallback` 避免不必要的重渲染
|
||||
5. **跨平台支持**:同时支持移动端触摸和桌面端鼠标操作
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 避免与滚动冲突
|
||||
|
||||
```tsx
|
||||
const { handlers } = useTouch({
|
||||
onSwipeLeft: handleSwipe,
|
||||
preventDefault: false, // 不阻止默认行为,允许页面滚动
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 性能优化
|
||||
|
||||
```tsx
|
||||
// 使用 useMemo 缓存配置
|
||||
const touchOptions = useMemo(
|
||||
() => ({
|
||||
onSwipe: handleSwipe,
|
||||
swipeThreshold: 50,
|
||||
}),
|
||||
[handleSwipe]
|
||||
);
|
||||
|
||||
const { handlers } = useTouch(touchOptions);
|
||||
```
|
||||
|
||||
### 3. 条件禁用
|
||||
|
||||
```tsx
|
||||
const { handlers } = useTouch({
|
||||
enabled: !isLoading, // 加载时禁用触摸
|
||||
onTap: handleTap,
|
||||
});
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```tsx
|
||||
import { useTouch } from "@/hooks";
|
||||
import { useState } from "react";
|
||||
|
||||
function TouchDemo() {
|
||||
const [log, setLog] = useState<string[]>([]);
|
||||
|
||||
const addLog = (message: string) => {
|
||||
setLog((prev) => [...prev, message].slice(-5));
|
||||
};
|
||||
|
||||
const { handlers, isTouching, touchData } = useTouch({
|
||||
onTap: () => addLog("点击"),
|
||||
onDoubleTap: () => addLog("双击"),
|
||||
onLongPress: () => addLog("长按"),
|
||||
onSwipeLeft: () => addLog("向左滑动"),
|
||||
onSwipeRight: () => addLog("向右滑动"),
|
||||
onSwipeUp: () => addLog("向上滑动"),
|
||||
onSwipeDown: () => addLog("向下滑动"),
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
{...handlers}
|
||||
style={{
|
||||
width: 300,
|
||||
height: 300,
|
||||
border: "2px solid #ccc",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
userSelect: "none",
|
||||
background: isTouching ? "#f0f0f0" : "white",
|
||||
}}
|
||||
>
|
||||
触摸测试区域
|
||||
{touchData && (
|
||||
<div style={{ fontSize: 12 }}>
|
||||
Δx: {touchData.deltaX.toFixed(0)}
|
||||
Δy: {touchData.deltaY.toFixed(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<h3>操作日志:</h3>
|
||||
{log.map((msg, i) => (
|
||||
<div key={i}>{msg}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
`useTouch` Hook 提供了:
|
||||
|
||||
- ✅ 完整的手势识别(点击、双击、长按、滑动)
|
||||
- ✅ 灵活的配置选项
|
||||
- ✅ 详细的触摸数据
|
||||
- ✅ 跨平台支持(触摸 + 鼠标)
|
||||
- ✅ TypeScript 类型安全
|
||||
- ✅ 性能优化
|
||||
|
||||
适用于各种触摸交互场景!
|
||||
@@ -0,0 +1,9 @@
|
||||
export { useDragAndDrop } from "./useDragAndDrop";
|
||||
export { usePageScale } from "./usePageScale";
|
||||
export { useTouch } from "./useTouch";
|
||||
export type {
|
||||
UseTouchOptions,
|
||||
UseTouchReturn,
|
||||
TouchEventData,
|
||||
SwipeDirection,
|
||||
} from "./useTouch";
|
||||
@@ -0,0 +1,485 @@
|
||||
import { useRef, useCallback, useState } from "react";
|
||||
|
||||
/**
|
||||
* 触摸点信息
|
||||
*/
|
||||
interface TouchPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 滑动方向
|
||||
*/
|
||||
export type SwipeDirection = "left" | "right" | "up" | "down";
|
||||
|
||||
/**
|
||||
* 手势事件回调参数
|
||||
*/
|
||||
export interface TouchEventData {
|
||||
/** 起始位置 */
|
||||
startPoint: TouchPoint;
|
||||
/** 当前位置 */
|
||||
currentPoint: TouchPoint;
|
||||
/** 移动距离 X */
|
||||
deltaX: number;
|
||||
/** 移动距离 Y */
|
||||
deltaY: number;
|
||||
/** 移动的总距离 */
|
||||
distance: number;
|
||||
/** 移动速度(像素/毫秒) */
|
||||
velocity: number;
|
||||
/** 移动方向 */
|
||||
direction: SwipeDirection | null;
|
||||
/** 原始事件 */
|
||||
event: React.TouchEvent | React.MouseEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* useTouch 配置选项
|
||||
*/
|
||||
export interface UseTouchOptions {
|
||||
/** 是否启用,默认 true */
|
||||
enabled?: boolean;
|
||||
|
||||
// ========== 滑动配置 ==========
|
||||
/** 触发滑动的最小距离(像素),默认 10 */
|
||||
swipeThreshold?: number;
|
||||
/** 触发快速滑动的最小速度(像素/毫秒),默认 0.5 */
|
||||
swipeVelocityThreshold?: number;
|
||||
|
||||
// ========== 长按配置 ==========
|
||||
/** 触发长按的时间(毫秒),默认 500 */
|
||||
longPressDelay?: number;
|
||||
/** 长按时允许的最大移动距离(像素),默认 10 */
|
||||
longPressMoveThreshold?: number;
|
||||
|
||||
// ========== 双击配置 ==========
|
||||
/** 双击的最大时间间隔(毫秒),默认 300 */
|
||||
doubleTapDelay?: number;
|
||||
/** 双击的最大移动距离(像素),默认 10 */
|
||||
doubleTapMoveThreshold?: number;
|
||||
|
||||
// ========== 事件回调 ==========
|
||||
/** 触摸开始 */
|
||||
onTouchStart?: (data: TouchEventData) => void;
|
||||
/** 触摸移动 */
|
||||
onTouchMove?: (data: TouchEventData) => void;
|
||||
/** 触摸结束 */
|
||||
onTouchEnd?: (data: TouchEventData) => void;
|
||||
/** 点击 */
|
||||
onTap?: (data: TouchEventData) => void;
|
||||
/** 双击 */
|
||||
onDoubleTap?: (data: TouchEventData) => void;
|
||||
/** 长按 */
|
||||
onLongPress?: (data: TouchEventData) => void;
|
||||
/** 滑动 */
|
||||
onSwipe?: (data: TouchEventData) => void;
|
||||
/** 向左滑动 */
|
||||
onSwipeLeft?: (data: TouchEventData) => void;
|
||||
/** 向右滑动 */
|
||||
onSwipeRight?: (data: TouchEventData) => void;
|
||||
/** 向上滑动 */
|
||||
onSwipeUp?: (data: TouchEventData) => void;
|
||||
/** 向下滑动 */
|
||||
onSwipeDown?: (data: TouchEventData) => void;
|
||||
|
||||
// ========== 其他配置 ==========
|
||||
/** 是否阻止默认行为,默认 false */
|
||||
preventDefault?: boolean;
|
||||
/** 是否阻止事件冒泡,默认 false */
|
||||
stopPropagation?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* useTouch 返回值
|
||||
*/
|
||||
export interface UseTouchReturn {
|
||||
/** 是否正在触摸 */
|
||||
isTouching: boolean;
|
||||
/** 触摸事件处理器 */
|
||||
handlers: {
|
||||
onTouchStart: (e: React.TouchEvent) => void;
|
||||
onTouchMove: (e: React.TouchEvent) => void;
|
||||
onTouchEnd: (e: React.TouchEvent) => void;
|
||||
onMouseDown: (e: React.MouseEvent) => void;
|
||||
};
|
||||
/** 当前触摸数据 */
|
||||
touchData: TouchEventData | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两点之间的距离
|
||||
*/
|
||||
function getDistance(p1: TouchPoint, p2: TouchPoint): number {
|
||||
const dx = p2.x - p1.x;
|
||||
const dy = p2.y - p1.y;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算滑动方向
|
||||
*/
|
||||
function getDirection(deltaX: number, deltaY: number): SwipeDirection | null {
|
||||
const absDeltaX = Math.abs(deltaX);
|
||||
const absDeltaY = Math.abs(deltaY);
|
||||
|
||||
if (absDeltaX < 10 && absDeltaY < 10) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (absDeltaX > absDeltaY) {
|
||||
return deltaX > 0 ? "right" : "left";
|
||||
} else {
|
||||
return deltaY > 0 ? "down" : "up";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 灵活的触摸手势处理 Hook
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { handlers, isTouching } = useTouch({
|
||||
* onSwipeLeft: () => console.log('向左滑动'),
|
||||
* onSwipeRight: () => console.log('向右滑动'),
|
||||
* onTap: () => console.log('点击'),
|
||||
* onDoubleTap: () => console.log('双击'),
|
||||
* onLongPress: () => console.log('长按'),
|
||||
* });
|
||||
*
|
||||
* return <div {...handlers}>可触摸区域</div>;
|
||||
* ```
|
||||
*/
|
||||
export function useTouch(options: UseTouchOptions = {}): UseTouchReturn {
|
||||
const {
|
||||
enabled = true,
|
||||
swipeThreshold = 10,
|
||||
swipeVelocityThreshold = 0.5,
|
||||
longPressDelay = 500,
|
||||
longPressMoveThreshold = 10,
|
||||
doubleTapDelay = 300,
|
||||
doubleTapMoveThreshold = 10,
|
||||
onTouchStart,
|
||||
onTouchMove,
|
||||
onTouchEnd,
|
||||
onTap,
|
||||
onDoubleTap,
|
||||
onLongPress,
|
||||
onSwipe,
|
||||
onSwipeLeft,
|
||||
onSwipeRight,
|
||||
onSwipeUp,
|
||||
onSwipeDown,
|
||||
preventDefault = false,
|
||||
stopPropagation = false,
|
||||
} = options;
|
||||
|
||||
// 状态
|
||||
const [isTouching, setIsTouching] = useState(false);
|
||||
const [touchData, setTouchData] = useState<TouchEventData | null>(null);
|
||||
|
||||
// 引用
|
||||
const startPointRef = useRef<TouchPoint | null>(null);
|
||||
const currentPointRef = useRef<TouchPoint | null>(null);
|
||||
const lastTapTimeRef = useRef<number>(0);
|
||||
const longPressTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const isLongPressTriggeredRef = useRef(false);
|
||||
const isMouseDownRef = useRef(false);
|
||||
|
||||
/**
|
||||
* 创建触摸点
|
||||
*/
|
||||
const createTouchPoint = useCallback(
|
||||
(x: number, y: number): TouchPoint => ({
|
||||
x,
|
||||
y,
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* 创建触摸事件数据
|
||||
*/
|
||||
const createTouchEventData = useCallback(
|
||||
(
|
||||
startPoint: TouchPoint,
|
||||
currentPoint: TouchPoint,
|
||||
event: React.TouchEvent | React.MouseEvent
|
||||
): TouchEventData => {
|
||||
const deltaX = currentPoint.x - startPoint.x;
|
||||
const deltaY = currentPoint.y - startPoint.y;
|
||||
const distance = getDistance(startPoint, currentPoint);
|
||||
const deltaTime = currentPoint.timestamp - startPoint.timestamp;
|
||||
const velocity = deltaTime > 0 ? distance / deltaTime : 0;
|
||||
const direction = getDirection(deltaX, deltaY);
|
||||
|
||||
return {
|
||||
startPoint,
|
||||
currentPoint,
|
||||
deltaX,
|
||||
deltaY,
|
||||
distance,
|
||||
velocity,
|
||||
direction,
|
||||
event,
|
||||
};
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* 清除长按定时器
|
||||
*/
|
||||
const clearLongPressTimer = useCallback(() => {
|
||||
if (longPressTimerRef.current) {
|
||||
clearTimeout(longPressTimerRef.current);
|
||||
longPressTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 处理触摸开始
|
||||
*/
|
||||
const handleStart = useCallback(
|
||||
(x: number, y: number, event: React.TouchEvent | React.MouseEvent) => {
|
||||
if (!enabled) return;
|
||||
|
||||
if (preventDefault) event.preventDefault();
|
||||
if (stopPropagation) event.stopPropagation();
|
||||
|
||||
const point = createTouchPoint(x, y);
|
||||
startPointRef.current = point;
|
||||
currentPointRef.current = point;
|
||||
isLongPressTriggeredRef.current = false;
|
||||
|
||||
setIsTouching(true);
|
||||
|
||||
const data = createTouchEventData(point, point, event);
|
||||
setTouchData(data);
|
||||
onTouchStart?.(data);
|
||||
|
||||
// 设置长按定时器
|
||||
if (onLongPress) {
|
||||
clearLongPressTimer();
|
||||
longPressTimerRef.current = setTimeout(() => {
|
||||
if (startPointRef.current && currentPointRef.current) {
|
||||
const distance = getDistance(
|
||||
startPointRef.current,
|
||||
currentPointRef.current
|
||||
);
|
||||
if (distance < longPressMoveThreshold) {
|
||||
isLongPressTriggeredRef.current = true;
|
||||
const longPressData = createTouchEventData(
|
||||
startPointRef.current,
|
||||
currentPointRef.current,
|
||||
event
|
||||
);
|
||||
onLongPress(longPressData);
|
||||
}
|
||||
}
|
||||
}, longPressDelay);
|
||||
}
|
||||
},
|
||||
[
|
||||
enabled,
|
||||
preventDefault,
|
||||
stopPropagation,
|
||||
createTouchPoint,
|
||||
createTouchEventData,
|
||||
onTouchStart,
|
||||
onLongPress,
|
||||
longPressDelay,
|
||||
longPressMoveThreshold,
|
||||
clearLongPressTimer,
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
* 处理触摸移动
|
||||
*/
|
||||
const handleMove = useCallback(
|
||||
(x: number, y: number, event: React.TouchEvent | React.MouseEvent) => {
|
||||
if (!enabled || !startPointRef.current) return;
|
||||
|
||||
if (preventDefault) event.preventDefault();
|
||||
if (stopPropagation) event.stopPropagation();
|
||||
|
||||
const point = createTouchPoint(x, y);
|
||||
currentPointRef.current = point;
|
||||
|
||||
const data = createTouchEventData(startPointRef.current, point, event);
|
||||
setTouchData(data);
|
||||
onTouchMove?.(data);
|
||||
|
||||
// 如果移动距离超过阈值,取消长按
|
||||
if (data.distance > longPressMoveThreshold) {
|
||||
clearLongPressTimer();
|
||||
}
|
||||
},
|
||||
[
|
||||
enabled,
|
||||
preventDefault,
|
||||
stopPropagation,
|
||||
createTouchPoint,
|
||||
createTouchEventData,
|
||||
onTouchMove,
|
||||
longPressMoveThreshold,
|
||||
clearLongPressTimer,
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
* 处理触摸结束
|
||||
*/
|
||||
const handleEnd = useCallback(
|
||||
(event: React.TouchEvent | React.MouseEvent) => {
|
||||
if (!enabled || !startPointRef.current || !currentPointRef.current)
|
||||
return;
|
||||
|
||||
if (preventDefault) event.preventDefault();
|
||||
if (stopPropagation) event.stopPropagation();
|
||||
|
||||
clearLongPressTimer();
|
||||
|
||||
const data = createTouchEventData(
|
||||
startPointRef.current,
|
||||
currentPointRef.current,
|
||||
event
|
||||
);
|
||||
onTouchEnd?.(data);
|
||||
|
||||
// 判断手势类型
|
||||
const { distance, velocity, direction } = data;
|
||||
|
||||
// 长按已触发,不处理其他手势
|
||||
if (isLongPressTriggeredRef.current) {
|
||||
setIsTouching(false);
|
||||
setTouchData(null);
|
||||
startPointRef.current = null;
|
||||
currentPointRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 滑动手势
|
||||
if (distance > swipeThreshold || velocity > swipeVelocityThreshold) {
|
||||
onSwipe?.(data);
|
||||
if (direction === "left") onSwipeLeft?.(data);
|
||||
if (direction === "right") onSwipeRight?.(data);
|
||||
if (direction === "up") onSwipeUp?.(data);
|
||||
if (direction === "down") onSwipeDown?.(data);
|
||||
}
|
||||
// 点击或双击
|
||||
else if (distance < doubleTapMoveThreshold) {
|
||||
const now = Date.now();
|
||||
const timeSinceLastTap = now - lastTapTimeRef.current;
|
||||
|
||||
if (timeSinceLastTap < doubleTapDelay && onDoubleTap) {
|
||||
// 双击
|
||||
onDoubleTap(data);
|
||||
lastTapTimeRef.current = 0; // 重置,避免三击触发两次双击
|
||||
} else {
|
||||
// 单击
|
||||
onTap?.(data);
|
||||
lastTapTimeRef.current = now;
|
||||
}
|
||||
}
|
||||
|
||||
setIsTouching(false);
|
||||
setTouchData(null);
|
||||
startPointRef.current = null;
|
||||
currentPointRef.current = null;
|
||||
},
|
||||
[
|
||||
enabled,
|
||||
preventDefault,
|
||||
stopPropagation,
|
||||
createTouchEventData,
|
||||
onTouchEnd,
|
||||
onSwipe,
|
||||
onSwipeLeft,
|
||||
onSwipeRight,
|
||||
onSwipeUp,
|
||||
onSwipeDown,
|
||||
onTap,
|
||||
onDoubleTap,
|
||||
swipeThreshold,
|
||||
swipeVelocityThreshold,
|
||||
doubleTapMoveThreshold,
|
||||
doubleTapDelay,
|
||||
clearLongPressTimer,
|
||||
]
|
||||
);
|
||||
|
||||
// 触摸事件处理器
|
||||
const handleTouchStart = useCallback(
|
||||
(e: React.TouchEvent) => {
|
||||
if (e.touches.length === 1) {
|
||||
const touch = e.touches[0];
|
||||
handleStart(touch?.clientX || 0, touch?.clientY || 0, e);
|
||||
}
|
||||
},
|
||||
[handleStart]
|
||||
);
|
||||
|
||||
const handleTouchMove = useCallback(
|
||||
(e: React.TouchEvent) => {
|
||||
if (e.touches.length === 1) {
|
||||
const touch = e.touches[0];
|
||||
handleMove(touch?.clientX || 0, touch?.clientY || 0, e);
|
||||
}
|
||||
},
|
||||
[handleMove]
|
||||
);
|
||||
|
||||
const handleTouchEnd = useCallback(
|
||||
(e: React.TouchEvent) => {
|
||||
handleEnd(e);
|
||||
},
|
||||
[handleEnd]
|
||||
);
|
||||
|
||||
// 鼠标事件处理器(桌面端支持)
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
isMouseDownRef.current = true;
|
||||
handleStart(e.clientX, e.clientY, e);
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (isMouseDownRef.current) {
|
||||
handleMove(
|
||||
event.clientX,
|
||||
event.clientY,
|
||||
e as unknown as React.MouseEvent
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = (event: MouseEvent) => {
|
||||
if (isMouseDownRef.current) {
|
||||
isMouseDownRef.current = false;
|
||||
handleEnd(e as unknown as React.MouseEvent);
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("mouseup", handleMouseUp);
|
||||
},
|
||||
[handleStart, handleMove, handleEnd]
|
||||
);
|
||||
|
||||
return {
|
||||
isTouching,
|
||||
handlers: {
|
||||
onTouchStart: handleTouchStart,
|
||||
onTouchMove: handleTouchMove,
|
||||
onTouchEnd: handleTouchEnd,
|
||||
onMouseDown: handleMouseDown,
|
||||
},
|
||||
touchData,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user