uniapp 微信小程序元素热区点击

407 阅读2分钟

起因:

这个模块样式和功能反复改,太tm烦人了。

只想说一句 产品傻逼

解决方案:

之前在开源程序交流群看到有人做过热区点击,所以也想自己试试。

一开始在小程序上面获取元素信息还费了点事,ref 还获取不到元素,问了下 AI 解决了。

  1. 先获点击的坐标相当于这个元素的位置

  2. 定义四个小区域范围

  3. 判断点击坐标是否在区域范围内,由于是小程序还要涉及单位换算,然后执行指定方法。

image.png

image.png

代码:

template

给元素设置个背景图片,样式代码就省略了。

<view class="mod" id="mod" @click="modClick($event)"> </view>

data里面定义数据

主要是相对于点击元素的 x,y 坐标,以及自身的宽高。

modList: [
        {
          id: 1,
          name: "收藏",
          info: {
            x: 50,
            y: 20,
            width: 108,
            height: 108,
          },
        },
        {
          id: 2,
          name: "积分",
          info: {
            x: 218,
            y: 20,
            width: 108,
            height: 108,
          },
        },
        {
          id: 3,
          name: "客服",
          info: {
            x: 386,
            y: 20,
            width: 108,
            height: 108,
          },
        },
        {
          id: 4,
          name: "红包卡卷",
          info: {
            x: 554,
            y: 20,
            width: 108,
            height: 108,
          },
        },
      ],

methods

modClick(e) {
      // 安全地获取第一个触摸点的坐标
      const touch =
        (e.touches && e.touches[0]) ||
        (e.changedTouches && e.changedTouches[0]);
      if (!touch) {
        console.error("No touch data available");
        return;
      }
      const x = touch.pageX;
      const y = touch.pageY;

      // 使用 uni-app 的选择器查询对象
      const query = uni.createSelectorQuery().in(this);
      query
        .select("#mod")
        .boundingClientRect((rect) => {
          if (!rect) {
            console.error("Element size could not be retrieved");
            return;
          }
          const relativeX = x - rect.left;
          const relativeY = y - rect.top;

          let clickedInside = false;
          let clickedItemName = "";

          // 遍历 modList 检查点击位置
          this.modList.forEach((item) => {
            // 确保 item.info 包含必要数据
            if (
              item.info &&
              item.info.x &&
              item.info.y &&
              item.info.width &&
              item.info.height
            ) {
              const isInside = isClickInsideSmallArea(item.info, {
                clickX: relativeX,
                clickY: relativeY,
              });
              if (isInside) {
                clickedInside = true;
                clickedItemName = item.name;
                return; // 找到匹配项,停止遍历
              }
            } else {
              console.warn(`Incomplete info data for item id: ${item.id}`);
            }
          });

          console.log("Clicked inside a small area:", clickedInside);
          if (clickedInside) {
            console.log("Clicked on:", clickedItemName);
            switch (clickedItemName) {
              case "收藏":
                this.handleCell(1);
                break;
              case "积分":
                break;
              case "客服":
                break;
              case "红包卡卷":
                break;

              default:
                break;
            }
          }
        })
        .exec();
    },

utils.js

/**
 * 将 rpx 转换为实际的 px
 * @param {number} rpx - 需要转换的 rpx 值
 * @param {number} screenWidth - 设备屏幕宽度,单位为px。如果未提供,则使用默认值。
 * @returns {number} 转换后的 px 值
 */
export function convertRpxToPx(rpx, screenWidth) {
  // 校验screenWidth是否为数字
  if (typeof screenWidth !== "number") {
    throw new TypeError("screenWidth must be a number");
  }

  // 边界条件处理
  if (rpx <= 0 || screenWidth <= 0) {
    throw new Error("rpx and screenWidth values must be greater than 0");
  }

  // 默认屏幕宽度,对应 iPhone6 的屏幕宽度
  const defaultScreenWidth = 375;

  // 使用提供的屏幕宽度,若无则使用默认值
  screenWidth = screenWidth || defaultScreenWidth;

  // 根据屏幕宽度转换 rpx 为 px
  const px = rpx * (screenWidth / 750);

  return px;
}

/**
 * 获取窗口信息并转换设计稿单位到实际显示单位的函数
 * @param {number} rpxValue - 需要转换的 rpx 值
 * @returns {Promise<number>} 转换后的 px 值的 Promise
 */
export function convertDesignToActual(rpxValue) {
  const windowInfo = wx.getWindowInfo();
  const actualScreenWidth = windowInfo.windowWidth;
  const pxValue = convertRpxToPx(rpxValue, actualScreenWidth);
  // 移除不必要的日志输出,或者改为可配置的日志级别控制
  // console.log(`${rpxValue}rpx 等于 ${pxValue.toFixed(2)}px`);
  return pxValue;
}

/**
 * 判断点击位置是否在小区域内
 * @param {Object} smallArea - 小区域的坐标和尺寸,单位为rpx
 * @param {number} clickX - 点击位置的x坐标,单位为px
 * @param {number} clickY - 点击位置的y坐标,单位为px
 * @returns {boolean} - 如果点击位置在小区域内返回true,否则返回false
 */
export function isClickInsideSmallArea(smallArea, { clickX, clickY }) {
  // 将小区域的坐标和尺寸从rpx转换为px
  const {
    x: smallAreaRpxX,
    y: smallAreaRpxY,
    width: smallAreaRpxWidth,
    height: smallAreaRpxHeight,
  } = smallArea;

  const smallAreaX = convertDesignToActual(smallAreaRpxX);
  const smallAreaY = convertDesignToActual(smallAreaRpxY);
  const smallAreaWidth = convertDesignToActual(smallAreaRpxWidth);
  const smallAreaHeight = convertDesignToActual(smallAreaRpxHeight);

  // 计算点击位置是否在小区域内
  const isInsideX =
    clickX >= smallAreaX && clickX <= smallAreaX + smallAreaWidth;
  const isInsideY =
    clickY >= smallAreaY && clickY <= smallAreaY + smallAreaHeight;

  // 如果点击位置在小区域的水平和垂直范围内,则返回 true
  return isInsideX && isInsideY;
}