首頁 > 軟體

react版模擬亞馬遜人機互動選單的實現

2022-02-08 13:00:12

前言

前段時間接了一個需求,實現一個模仿亞馬遜和京東的選單互動效果,這種效果被稱為模擬人機互動。在網上搜了一下,目前沒有見到有 reactVue的版本,然後就自己參考了一下現有的方式,實現了一個react版本。

需求介紹

本文都是在web端的需求

參考亞馬遜和京東商城的首頁左側選單效果,實現一個react版本的元件,以供業務使用。

我們先看下亞馬遜和京東商城的效果:

亞馬遜商城

京東商城

從上面的效果得出我們的選單效果需求點:

  • 當我們的滑鼠懸浮在左側選單的時候,右側會對應展示它對應的子選單項,
  • 當我們的滑鼠在左側選單上下移動時,左側可以快速切換為對應的子選單
  • 當我們的滑鼠移動以一定的傾斜角度移動到右側的時候,滑鼠雖然會經過其它的左側選單,但是不會執行切換。

到目前為止,我們就搞情況了我們的需求。接下來就要去實現我們的方案了。

實現方案

要實現我們的需求,複雜點主要是在如何實現上述的需求3。需求1和需求2 的基本切換效果我就不再說了,直接進入需求3

實現需求3

如果要實現這個需求,我們需要記錄滑鼠從左往右(從左側選單區域移動到右側選單區域)的移動軌跡,然後根據它的移動軌跡去判斷它是否是在一個三角形的區域之內,如果在的話,就不讓它切換選單。

我們先看一張圖:

  • P1:滑鼠的起始位置
  • P2:左側選單的固定點1,滑鼠在左側區域的最大位移點
  • P3:左側選單的固定點2,滑鼠在左側區域的最大位移點
  • M:滑鼠在左側選單移動的結束位置

從上圖我們可以得出:

如果滑鼠的起始點是在 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)
    }
  }

實現效果

 到此這篇關於react版模擬亞馬遜人機互動選單的實現的文章就介紹到這了,更多相關react 互動選單內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com