index.vue 7.67 KB
<template>
  <div class="heat-map" style="background-color: #fff">
    <div class="canvas">
      <img
        :src="floorImage"
        class="editFloorimg"
        id="editFloorimg"
        style="width: 100%"
      />
      <div class="canvas-position" id="canvas-position"></div>
    </div>
  </div>
  <div
    style="width: 90%; margin-top: 20px; padding: 0 20px"
    v-if="channelList.length"
  >
    <van-slider
      bar-height="15px"
      :max="sliderMax"
      :min="1"
      v-model="sliderVal"
      @change="slideHandle"
    />
  </div>
</template>
<script setup>
import { onMounted, watch, ref, nextTick } from "vue";
import { useRoute } from "vue-router";
import { Toast } from "vant";

import heatmap from "@/api/heatmap"; // 假设你的API文件路径
import { heatmapFactory } from "@/utils/heatmap.js";
let map = null; // 地图对象

const $route = useRoute();

// 监听route参数
// ?atoken='xxx'&mallId='xxx'
const storeId = ref(""); // 店铺id
const channelList = ref([]); // 渠道列表
const getChannelList = async (mallId) => {
  try {
    const { data } = await heatmap.getChannelsListApi({ mallId, status: 1 });

    if (data.code === 200) {
      channelList.value =
        data.data?.map((item) => {
          return item.serialnum;
        }) || [];
      if (channelList.value.length) {
        getHeatMapData({
          mallId,
          channelList: channelList.value,
        });
      } else {
        Toast.fail("无热力数据");
      }
    } else {
      console.error("Failed to fetch channel list:", data.message);
      channelList.value = [];
    }
  } catch (error) {
    console.error("Error fetching channel list:", error);
  }
};
// 获取热力图数据
const getHeatMapData = async (params) => {
  try {
    const options = {
      channelSerialnum_arr: params.channelList,
      type: 2,
      genders: [1, 0],
      ages: ["0-18", "19-35", "36-55", "56-100"],
      personTypes: [0],
      counttime_gte: `${startDate.value} 00:00:00`,
      counttime_lte: `${endDate.value} 23:59:59`,
      countdate: endDate.value,
      mallId: params.mallId,
    };

    const { data } = await heatmap.getHeatMapValueApi(options);
    if (data.code === 200) {
      heatDataObj.value = data.data || [];
      nextTick(() => {
        dealHeatData();
      });
    } else {
    }
  } catch (error) {
    console.error("Error fetching heat map data:", error);
    return [];
  }
};

/************** 图片相关 **************/
const floorImage = ref("");
const getFloorImage = async () => {
  try {
    console.log(storeId.value);
    const { data } = await heatmap.getStoreDataApi(storeId.value);
    if (data.code === 200) {
      return data.data?.mallPlan || "";
    }
    return "";
  } catch (error) {
    return "";
  }
};

/************** 热力图相关 **************/
const heatInstance = ref(null);
const heatRadius = ref(10); // 热力图半径
const heatDataObj = ref([]); // 热力图数据对象
const timeLevel = ref("rt"); // 时间级别,默认实时 rt:停留时长 rc:顾客人次
const normalWidth = ref(100);
const startDate = ref(""); // 开始时间
const endDate = ref(""); // 结束时间

const interpolationCache = ref([]); // 插值缓存
const pointsData = ref([]); // 热力图点数据
const sliderMax = ref(0); // 滑块最大值
const sliderVal = ref(0); // 滑块当前值
const marks = ref({}); // 滑块标记
function dealHeatData() {
  try {
    const img = document.getElementById("editFloorimg");
    if (!img.complete || img.naturalWidth === 0) {
      // 等待图片加载完成
      img.onload = () => dealHeatData();
      return;
    }

    if (heatInstance.value) {
      heatInstance.value.destroy();
    }
    let { width, height, naturalWidth, naturalHeight } =
      document.getElementById("editFloorimg");
    let radix = 0;
    if (naturalWidth === 0 || naturalHeight === 0) {
      return;
    }
    if (naturalWidth !== 0 && naturalHeight !== 0) {
      radix = width / naturalWidth;
      if (heatInstance.value) {
        heatInstance.value.setData({ data: [] });
      }
      let heatDom = document.getElementById("canvas-position");
      heatDom.style.width = width + "px";
      heatDom.style.height = height + "px";
      heatInstance.value = heatmapFactory.create({
        container: heatDom,
        radius: heatRadius.value,
        onExtremaChange: () => {},
      });
      let points = [];
      let maxVal = 1;
      heatDataObj.value.forEach((item, index) => {
        const x = parseInt((item.rx / normalWidth.value) * width);
        const y = parseInt((item.ry / normalWidth.value) * height);
        maxVal =
          maxVal > item[timeLevel.value] ? maxVal : item[timeLevel.value];
        points.push({
          x: combinePoint(x),
          y: combinePoint(y),
          value: item[timeLevel.value] || 1,
        });
      });
      let newPoints = [];
      points.forEach((one) => {
        let isAdd = true;
        for (let i = 0; i < newPoints.length; i++) {
          if (newPoints[i].x == one.x && newPoints[i].y == one.y) {
            newPoints[i].value = newPoints[i].value + one.value;
            isAdd = false;
            break;
          }
        }
        if (isAdd) {
          newPoints.push({
            x: one.x,
            y: one.y,
            value: one.value,
          });
        }
      });

      newPoints.forEach((item, index) => {
        maxVal = maxVal > item.value ? maxVal : item.value;
      });

      interpolationCache.value = [];
      // let maxVal = Math.max(..._.pluck(points, "value")) * 5;
      // maxVal = maxVal*10;
      pointsData.value = newPoints;
      heatInstance.value.setData({ data: newPoints });
      heatInstance.value.setDataMax(maxVal);
      let newMax =
        timeLevel.value == "rt"
          ? (parseInt(maxVal / 3600) * 1 + 1) * 3600
          : maxVal;
      let newVal = newMax / 4;
      // 固定最大值好默认值
      sliderMax.value = newMax;
      sliderVal.value = newVal;
      slideHandle(sliderVal.value);
    }
    uni.postMessage({
      type: "loadSuccess",
    });
  } catch (error) {
    console.error("Error in dealHeatData:", error);
  } finally {
    Toast.clear();
  }
}
function combinePoint(point, pointToler = 8) {
  return parseInt(point - (point % pointToler));
}
function slideHandle(val) {
  marks.value = {};
  marks.value[val] =
    timeLevel.value == "rt" ? getTimeMin(val) : Math.round(val) + "";
  heatInstance.value.setDataMax(sliderVal.value);
}
function getTimeMin(seconds) {
  if (isNaN(seconds)) return seconds;
  return (
    numFormat(parseInt(seconds / 3600)) +
    ":" +
    numFormat(parseInt((seconds % 3600) / 60)) +
    ":00"
  );
}
function numFormat(val) {
  return val > 9 ? val : "0" + val;
}

watch(
  () => $route.query,
  async (newVal) => {
    const { token, mallId, startDate: sDate, endDate: eDate, level } = newVal;

    if (token) {
      window.localStorage.setItem("atoken", token);
    }
    startDate.value = sDate || new Date().toISOString().split("T")[0];
    endDate.value = eDate || new Date().toISOString().split("T")[0];
    if (mallId) {
      timeLevel.value = level || "rt"; // 默认实时
      Toast.loading({
        message: "加载中...",
        forbidClick: true,
        duration: 0,
      });
      storeId.value = mallId;
      const url = await getFloorImage();
      if (!url) {
        Toast.fail("楼层图片未找到,请检查mallId");
        return false;
      }
      floorImage.value = `https://store.keliuyun.com/images/${url}`;
      if (floorImage.value) {
        getChannelList(mallId);
      }
    }
  },
  { immediate: true }
);
</script>
<style>
.canvas {
  position: relative;
  width: 100%;
}
.canvas .canvas-position {
  position: absolute !important;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
}
</style>