时间:2023-02-02 10:13:38 | 栏目: | 点击:次
前段时间接了一个需求,实现一个模仿亚马逊和京东的菜单交互效果,这种效果被称为模拟人机交互。在网上搜了一下,目前没有见到有 react
和Vue
的版本,然后就自己参考了一下现有的方式,实现了一个react
版本。
本文都是在web端的需求
参考亚马逊和京东商城的首页左侧菜单效果,实现一个react版本的组件,以供业务使用。
我们先看下亚马逊和京东商城的效果:
亚马逊商城
京东商城
从上面的效果得出我们的菜单效果需求点:
到目前为止,我们就搞情况了我们的需求。接下来就要去实现我们的方案了。
要实现我们的需求,复杂点主要是在如何实现上述的需求3。需求1和需求2 的基本切换效果我就不再说了,直接进入需求3
如果要实现这个需求,我们需要记录鼠标从左往右(从左侧菜单区域移动到右侧菜单区域)的移动轨迹,然后根据它的移动轨迹去判断它是否是在一个三角形的区域之内,如果在的话,就不让它切换菜单。
我们先看一张图:
从上图我们可以得出:
如果鼠标的起始点是在 P1 的话,当鼠标移动到右侧区域,鼠标可能经过的三角形区域就是 P1-P2-P3所在的三角形,M点是鼠标的结束位置。所以我们判断鼠标的运动轨迹是否在三角形中就可以了。
const [active, setActive] = useState(null) // 选中的菜单 const [showSub, setShowSub] = useState(false) // 是否显示子菜单 let timeout = useRef(null) // 设置延迟定时器,防止鼠标移到tab内容经过菜单时的切换 let mouseLocs = useRef([]) // 记录鼠标移动时的坐标数组 let firstSlope = useRef(null) // 菜单栏的固定点1, 根据菜单栏和内容的位置而改变 let secondSlope = useRef(null) // 菜单栏的固定点2, 根据菜单栏和内容的位置而改变 const refNavigation = useRef(null) const refNav = useRef(null) const refSubnav = useRef(null) /** * 根据内容栏相对于菜单栏的位置,判断移动过程中的点是否在三角形内 * @param {Object} p1 开始位置 * @param {Object} p2 菜单栏固定点1 * @param {Object} p3 菜单栏固定点2 * @param {Object} m 结束位置 * @return {*} */ function proPosInTriangle(p1, p2, p3, m) { // 结束时鼠标坐标位置 let x = m.x, y = m.y, // 开始鼠标坐标位置 x1 = p1.x, y1 = p1.y, // 菜单栏包裹层右上角坐标 x2 = p2.x, y2 = p2.y, // 右下角坐标 x3 = p3.x, y3 = p3.y, // (y2 - y1) / (x2 - x1)为两坐标连成直线的斜率 // 因为直线的公式为y=kx+b;当斜率相同时,只要比较 // b1和b2的差值就可以知道该点是在 // (x1,y1),(x2,y2)的直线的哪个方向 // 当r1大于0,说明该点在直线右侧,其它以此类推 r1 = y - y1 - ((y2 - y1) / (x2 - x1)) * (x - x1), r2 = y - y2 - ((y3 - y2) / (x3 - x2)) * (x - x2), r3 = y - y3 - ((y1 - y3) / (x1 - x3)) * (x - x3), compare compare = r1 * r2 * r3 < 0 && r1 > 0 // 返回是否在三角形内的结果 return compare } /** * 获取元素相对于浏览器左上角的坐标位置,为正值 * @param element * @return {{x: Number, y: Number}} * @constructor */ function LocFromdoc(element) { const { x, y, width, height } = element.getBoundingClientRect() return { x: x, y: y, width, height, } } /** * 记录元素的位置信息 * @param element * @return {{top: *, topAndHeight: number, left: *, leftAndWidth: number}} */ function getInfo(element) { const location = LocFromdoc(element) return { top: location.y, topAndHeight: location.y + element.offsetHeight, // offsetHeight 元素的像素高度, 高度包含该元素的垂直内边距和边框,且是一个整数 left: location.x, leftAndWidth: location.x + element.offsetWidth, } } /** * 根据内容栏相对于菜单栏的位置, 返回菜单栏的固定点1,和固定点2,保存在this.firstSlope和this.secondSlope对象里 * 即 左侧菜单栏的右上角和右下角的位置 */ function ensureTriangleDots() { // 获取菜单栏的位置 const info = getInfo(refNav.current) const x1 = info.leftAndWidth const y1 = info.top const x2 = x1 const y2 = info.topAndHeight firstSlope.current = { x: x1, y: y1, } secondSlope.current = { x: x2, y: y2, } } const onMouseOver = useCallback( obj => { let diff try { // 是否在指定三角形内 diff = proPosInTriangle( mouseLocs.current[0], firstSlope.current, secondSlope.current, mouseLocs.current[2] ) } catch (ex) {} // 是就启动延迟显示, // 否则不延迟 if (diff) { timeout.current = setTimeout(() => { setActive(obj.key) setShowSub(true) }, 300) } else { setActive(obj.key) setShowSub(true) } }, [mouseLocs, timeout] ) const onMouseEnter = () => { // 计算位置 if (refNav.current) { ensureTriangleDots() } setShowSub(true) } // 移出菜单所在区域 const onMouseLeave = () => { if (refSubnav.current) { setActive(null) setShowSub(false) } } // 记录鼠标在菜单栏中移动的最后三个坐标位置 const onMousemove = event => { mouseLocs.current.push({ x: event.pageX, y: event.pageY, }) if (mouseLocs.current.length > 3) { // 移除超过三项的数据 mouseLocs.current.shift() } } // 鼠标移出的时候,清除延时器 const onMouseout = () => { if (timeout.current) { clearTimeout(timeout.current) } }