React移动端悬浮球组件

931 阅读1分钟
import React, { useEffect, useState, FC } from 'react';
import { useGesture } from '@use-gesture/react';
import { useSpring, animated } from '@react-spring/web';

interface IProps {
    scrolling: boolean;
}

const FloatingBall: FC<IProps> = props => {
    const [show, setShow] = useState<boolean>(false);
    const [positionType, setPositionType] = useState<string>('right');

    const { scrolling = false } = props;
    const [style, setSpringStyle] = useSpring(() => ({ x: 0, y: 0 }));
    const clientWidth: number = document.documentElement.clientWidth || document.body.clientWidth;
    const clientHeight: number = document.documentElement.clientHeight || document.body.clientHeight;

    // 悬浮球常量
    const STOW_OFFSET: number = 60; // 悬浮球收起偏移量
    const FLOATING_BALL_WIDTH: number = 65; // 悬浮球宽度
    const FLOATING_BALL_HEIGHT: number = 65; // 悬浮球高度
    const FLOATING_BALL_MARGIN: number = 10; // 悬浮球初始位置距离右侧、底部距离

    // 悬浮球停靠位置范围
    const maxX: number = 0; // 悬浮球停靠右侧位置
    const maxY: number = 0; // 悬浮球停靠低部位置
    const minX: number = -clientWidth + FLOATING_BALL_WIDTH + FLOATING_BALL_MARGIN * 2; // 悬浮球停靠左侧位置
    const minY: number = -clientHeight + FLOATING_BALL_HEIGHT + FLOATING_BALL_MARGIN * 2; // 悬浮球停靠顶部位置
    
    useEffect(() => {
        let _x: number | null = null;
        const isPositionLeft: boolean = positionType === 'left';

        if (scrolling) {
            _x = isPositionLeft ? minX - STOW_OFFSET : maxX + STOW_OFFSET;
        } else {
            _x = isPositionLeft ? minX : maxX;
        }

        setSpringStyle({ x: _x });
    }, [scrolling]);

    // 拖动悬浮球
    const bind = useGesture({
        onDrag: ({ offset: [x, y] }) => {
            setSpringStyle({ x, y });
        },
        onDragEnd: ({ offset, offset: [x, y] }) => {
            // 判断悬浮球停靠左侧还是右侧
            const _positionType: string = x < -(clientWidth - FLOATING_BALL_WIDTH - FLOATING_BALL_MARGIN * 2) / 2 ? 'left' : 'right';

            // 计算悬浮球停靠位置
            const _x: number = _positionType === 'left' ? minX : maxX;
            const _y: number = y > maxY ? maxY : y < minY ? minY : y;

            // 保存悬浮球新的停靠位置,下次拖动以此位置为基础
            offset[0] = _x;
            offset[1] = _y;

            setPositionType(_positionType);
            setSpringStyle({ x: _x, y: _y });
        },
    });

    return (
            <animated.div {...bind()} style={style} className="floating-ball-box">
                <img
                    className="floating-ball-icon"
                    src={require('@/images/seeDoctor/floating-ball-icon.png')}
                />
            </animated.div>
    );
};

export default FloatingBall;

// 悬浮球相关样式
.floating-ball-box {
    z-index: 30;
    width: 65px;
    height: 65px;
    cursor: pointer;
    touch-action: none; // 此样式必须有
    position: fixed;
    right: 10px;
    bottom: 10px;

    .floating-ball-icon {
        width: 100%;
        height: 100%;
    }
}