index.vue 9.24 KB
<template>
  <div>
    <div class="heat-map" style="background-color: #fff">
      <div class="canvas">
        <img
          :src="floorImage"
          class="editFloorimg"
          id="editFloorimg"
          style="width: 100%"
        />
        <canvas class="canvas-position" id="canvas-position"></canvas>
      </div>
    </div>
    <div class="color-legend" v-if="floorImage">
      <div v-for="(item, index) in gateLegend" :key="index" class="color-box">
        <p
          class="color-text"
          :style="{ top: (index + 1) % 2 == 0 ? '19px' : '-21px' }"
        >
          {{ item.text }}
        </p>
        <span
          class="color-block"
          :style="{ 'background-color': item.color }"
        ></span>
        <span
          class="border-span"
          :style="{ top: (index + 1) % 2 == 0 ? '14px' : '-5px' }"
        ></span>
      </div>
    </div>
  </div>
</template>

<script setup>
import { watch, ref } from "vue";
import { useRoute } from "vue-router";
import { Toast } from "vant";
import heatmap from "@/api/heatMap";

const $route = useRoute();

const storeId = ref(""); // 店铺id
const startDate = ref(""); // 开始日期
const endDate = ref(""); // 结束日期
const indicatorKey = ref(""); // 时间级别,默认实时

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

/************** 通道数据相关 **************/
const channelList = ref([]); // 渠道列表
const getChannelList = async (mallId) => {
  try {
    const { data } = await heatmap.getChannelsListApi({ mallId, status: 1 });

    if (data.code === 200) {
      channelList.value = data.data || [];
      if (channelList.value.length > 0) {
        // 获取统计数据
        getGateStatistics();
      }
    } else {
      channelList.value = [];
    }
  } catch (error) {}
};
// 获取区域数据
const gateData = ref([]);
const getGateStatistics = async () => {
  try {
    const params = {
      mallId: storeId.value,
      startDate: startDate.value,
      endDate: endDate.value,
    };
    const { data } = await heatmap.getAreaGateStatisticsApi(params);
    if (data.code === 200) {
      gateData.value = data.data || [];
      processGateLegend();
      if (gateData.value.length > 0) {
        drawAreaCanvas();
      }
    }
  } catch (error) {
    console.error("获取区域数据失败:", error);
  }
};

// 渲染canvas
const areas = ref([]); // 区域数据
function drawAreaCanvas() {
  const img = document.getElementById("editFloorimg");
  if (!img.complete || img.naturalWidth === 0) {
    // 等待图片加载完成
    img.onload = () => drawAreaCanvas();
    return;
  }
  let _width = img.width;
  let _height = img.height;

  let canvasEle = document.getElementById("canvas-position");
  let ctx = canvasEle.getContext("2d");
  ctx.lineWidth = 3;
  ctx.clearRect(0, 0, _width, _height);
  canvasEle.width = _width;
  canvasEle.height = _height;
  ctx.globalAlpha = 0.5;
  channelList.value.forEach((channelItem) => {
    let info = channelItem.rAreaInfo ? JSON.parse(channelItem.rAreaInfo) : null;
    let linexysets = info ? info.linexysets : [];

    let gateObj = gateData.value.find(
      (item) => channelItem.gateId == item.gateId
    );
    let personMantime =
      gateObj && gateObj[indicatorKey.value]
        ? gateObj[indicatorKey.value]
        : null;

    if (linexysets && linexysets.length > 0) {
      const path = new Path2D();
      let originX = Number(((linexysets[0].x * _width) / 1920).toFixed(2));
      let originY = Number(((linexysets[0].y * _height) / 1080).toFixed(2));
      let minX = originX,
        maxX = originX,
        minY = originY,
        maxY = originY;

      linexysets.forEach((item, index) => {
        // 适配画布实际坐标
        const x = Number(((item.x * _width) / 1920).toFixed(2));
        const y = Number(((item.y * _height) / 1080).toFixed(2));

        // 记录x、y的最大最小值
        minX = Math.min(minX, x);
        maxX = Math.max(maxX, x);
        minY = Math.min(minY, y);
        maxY = Math.max(maxY, y);

        if (index === 0) {
          path.moveTo(x, y);
        } else {
          path.lineTo(x, y);
        }
      });

      path.closePath();

      // 计算中心点位置
      let centerX = (minX + maxX) / 2;
      let centerY = (minY + maxY) / 2;
      areas.value.push({
        path,
        gateId: channelItem.gateId,
        x: centerX,
        y: centerY,
      });

      ctx.strokeStyle = colorFormat(personMantime);
      ctx.lineWidth = 2;
      ctx.fillStyle = colorFormat(personMantime);
      ctx.fill(path);
      ctx.stroke(path);
    }
  });
  // 点击区域后,得到区域信息展示在页面中
  canvasEle.onclick = (e) => {
    const rect = canvasEle.getBoundingClientRect();
    const scaleX = canvasEle.width / rect.width;
    const scaleY = canvasEle.height / rect.height;
    const x = (e.clientX - rect.left) * scaleX;
    const y = (e.clientY - rect.top) * scaleY;
    let selectedArea = areas.value.find((area) => {
      return ctx.isPointInPath(area.path, x, y);
    });
    uni.postMessage(
      JSON.parse(
        JSON.stringify({
          type: "areaClick",
          data: selectedArea ? selectedArea : null,
        })
      )
    );
  };
}

function processGateLegend() {
  let legendData = [],
    max = 20;
  gateData.value.forEach((item) => {
    legendData.push(item[indicatorKey.value] || 0);
  });
  max = Math.max.apply(null, legendData);
  max = Math.ceil(max);
  let num = Number(numFun(max)[0]);
  let numLen = numFun(max).length - 1;
  let maxNum = Number(num) * Math.pow(10, numLen);
  if (
    [
      "totalResidenceTime",
      "avgResidenceTime",
      "validDwellTime",
      "avgValidDwellTime",
    ].includes(indicatorKey.value)
  ) {
    let maxMin = Math.floor(maxNum / 60);
    // console.log('maxMin',maxMin)
    if (maxMin >= 5) {
      let unit = parseInt(maxMin / 5) * 60;
      gateLegend.value[6].day = maxMin * 60 + unit;
      gateLegend.value[6].text = formatSecondsMin(maxNum + unit) + "+";
      for (let i = 1; i < gateLegend.value.length - 1; i++) {
        gateLegend.value[i].text = formatSecondsMin(unit * i);
        gateLegend.value[i].day = unit * i;
      }
    } else {
      let unit = parseInt(maxNum / 5);
      gateLegend.value[6].day = maxNum + unit;
      gateLegend.value[6].text = maxNum + unit + "s+";
      for (let i = 1; i < gateLegend.value.length - 1; i++) {
        gateLegend.value[i].text = unit * i + "s";
        gateLegend.value[i].day = unit * i;
      }
    }
  } else {
    let unit = maxNum / 5;
    gateLegend.value[6].day = maxNum + unit;
    gateLegend.value[6].text = maxNum + unit + "+";
    for (let i = 1; i < gateLegend.value.length - 1; i++) {
      gateLegend.value[i].text = unit * i;
      gateLegend.value[i].day = unit * i;
    }
  }
}
const gateLegend = ref([
  { color: "#A0DDCB", text: 0, day: 0 },
  { color: "#90E985", text: 100, day: 100 },
  { color: "#B0FC0D", text: 200, day: 200 },
  { color: "#FAF817", text: 300, day: 300 },
  { color: "#FEAD11", text: 400, day: 400 },
  { color: "#FF3C02", text: 500, day: 500 },
  { color: "#FC0000", text: "500+", day: "500" },
]);
function colorFormat(val) {
  if (val === null) {
    return false;
  }
  let color = "";
  let changeLegend = gateLegend.value;
  for (var i = 1; i < changeLegend.length; i++) {
    if (i == changeLegend.length - 1 && val >= changeLegend[i].day) {
      color = changeLegend[i].color;
      break;
    } else if (val >= changeLegend[i - 1].day && val < changeLegend[i].day) {
      color = changeLegend[i - 1].color;
      break;
    }
  }
  return color;
}
function numFun(num) {
  if (num > 19) {
    let s = "" + num;
    let res = [];
    for (let i = 0; i < s.length; i++) {
      res.push(s[i]);
    }
    return res;
  } else {
    return [1, 0];
  }
}
function formatSecondsMin(val) {
  if (isNaN(val)) return val;
  return parseInt(val / 60) + "m";
}

watch(
  () => $route.query,
  async (newVal) => {
    const { token, mallId, startDate: sDate, endDate: eDate, key } = 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) {
      indicatorKey.value = key;

      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;
}
.color-legend {
  width: auto;
  height: auto;
  margin-top: 20px;
}
.color-box {
  float: left;
  position: relative;
}
.color-block {
  float: left;
  width: 60px;
  height: 40px;
}

.color-text {
  width: 100%;
  text-align: center;
  font-size: 1em;
  left: -15px;
  position: absolute;
}
</style>