Commit 644bca40 by 陈岩

feat: 完成区域热力功能

1 parent 7f68aed8
......@@ -13,5 +13,8 @@ const heatmap = {
getStoreDataApi(mallId) {
return req("get", `/report/b-mall/${mallId}`);
},
getAreaGateStatisticsApi(params, config) {
return req("get", `/report/gate/analyse/statistics`, params, config);
},
};
export default heatmap;
......@@ -12,6 +12,11 @@ const routes = [
name: "HeatMap",
component: () => import("@/views/heatMap/index.vue"),
},
{
path: "/areaHeatMap",
name: "AreaHeatMap",
component: () => import("@/views/areaHeat/index.vue"),
},
];
const router = createRouter({
......
<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>
......@@ -7,14 +7,8 @@
id="editFloorimg"
style="width: 100%"
/>
<div
class="canvas-position"
id="canvas-position"
@mousemove="mousemoveHandle"
@mouseout="mouseoutHandle"
></div>
<div class="canvas-position" id="canvas-position"></div>
</div>
<!-- <el-slider v-model="sliderVal" :marks="marks" :format-tooltip="formatTooltip" :max="sliderMax" :min="1" vertical @change="slideHandle(sliderVal)" height="200px"></el-slider> -->
</div>
<div
style="width: 90%; margin-top: 20px; padding: 0 20px"
......@@ -99,6 +93,7 @@ const getHeatMapData = async (params) => {
};
/************** 图片相关 **************/
const floorImage = ref("");
const getFloorImage = async () => {
try {
console.log(storeId.value);
......@@ -111,7 +106,6 @@ const getFloorImage = async () => {
return "";
}
};
const floorImage = ref("");
/************** 热力图相关 **************/
const heatInstance = ref(null);
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!