index.vue 6.56 KB
<template>
  <div>
    <div class="canvas">
      <img
        :src="floorImage"
        class="editFloorimg"
        id="editFloorimg"
        style="width: 100%"
      />
      <div
        class="canvas-position"
        id="canvas-position"
        @mousemove="mousemoveHandle"
        @mouseout="mouseoutHandle"
      ></div>
    </div>
    <!-- <el-slider v-model="sliderVal" :marks="marks" :format-tooltip="formatTooltip" :max="sliderMax" :min="1" vertical @change="slideHandle(sliderVal)" height="200px"></el-slider> -->

    <van-slider
      bar-height="20px"
      :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 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 {
      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: "2025-05-29 10:00:00",
      counttime_lte: "2025-05-29 22:00:00",
      countdate: "2025-05-29",
      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 =
  "https://store.keliuyun.com/images/report/mallPic/32a04280-5a32-4b0e-a2a5-15874da46a1120240826163115.jpg"; // 楼层图片

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

const interpolationCache = ref([]); // 插值缓存
const pointsData = ref([]); // 热力图点数据
const sliderMax = ref(0); // 滑块最大值
const sliderVal = ref(0); // 滑块当前值
const marks = ref({}); // 滑块标记
function dealHeatData() {
  const img = document.getElementById("editFloorimg");
  console.log(img.complete, img.naturalWidth);
  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: function (data) {
        console.log(data);
      },
    });
    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;
    console.log(newPoints, "--s");
    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);
  }
}
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,
  (newVal) => {
    const { token, mallId } = newVal;
    if (token) {
      window.localStorage.setItem("atoken", token);
    }
    if (mallId) {
      storeId.value = mallId;
      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>