mirror of
http://git.keliuyun.com:55676/jiaxiuc123/miniProject.git
synced 2025-09-26 19:31:23 +08:00
feat: 完成区域热力功能
This commit is contained in:
@@ -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({
|
||||
|
353
h5/src/views/areaHeat/index.vue
Normal file
353
h5/src/views/areaHeat/index.vue
Normal file
@@ -0,0 +1,353 @@
|
||||
<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);
|
||||
|
Reference in New Issue
Block a user