- 新增引导页组件(Guide.tsx)及相关资源 - 添加视频播放功能和相关资源文件 - 实现useTouch自定义hook支持触摸交互 - 添加store状态管理 - 更新多个页面组件(Content, Menu, Publicity) - 优化图片组件和背景图片组件 - 更新项目依赖
486 lines
12 KiB
TypeScript
486 lines
12 KiB
TypeScript
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,
|
|
};
|
|
}
|