mirror of
http://git.keliuyun.com:55676/jiaxiuc123/miniProject.git
synced 2025-09-27 03:35:59 +08:00
feat: 完成区域热力功能
This commit is contained in:
@@ -13,5 +13,8 @@ const heatmap = {
|
|||||||
getStoreDataApi(mallId) {
|
getStoreDataApi(mallId) {
|
||||||
return req("get", `/report/b-mall/${mallId}`);
|
return req("get", `/report/b-mall/${mallId}`);
|
||||||
},
|
},
|
||||||
|
getAreaGateStatisticsApi(params, config) {
|
||||||
|
return req("get", `/report/gate/analyse/statistics`, params, config);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
export default heatmap;
|
export default heatmap;
|
||||||
|
@@ -12,6 +12,11 @@ const routes = [
|
|||||||
name: "HeatMap",
|
name: "HeatMap",
|
||||||
component: () => import("@/views/heatMap/index.vue"),
|
component: () => import("@/views/heatMap/index.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/areaHeatMap",
|
||||||
|
name: "AreaHeatMap",
|
||||||
|
component: () => import("@/views/areaHeat/index.vue"),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
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"
|
id="editFloorimg"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
<div
|
<div class="canvas-position" id="canvas-position"></div>
|
||||||
class="canvas-position"
|
|
||||||
id="canvas-position"
|
|
||||||
@mousemove="mousemoveHandle"
|
|
||||||
@mouseout="mouseoutHandle"
|
|
||||||
></div>
|
|
||||||
</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>
|
||||||
<div
|
<div
|
||||||
style="width: 90%; margin-top: 20px; padding: 0 20px"
|
style="width: 90%; margin-top: 20px; padding: 0 20px"
|
||||||
@@ -99,6 +93,7 @@ const getHeatMapData = async (params) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/************** 图片相关 **************/
|
/************** 图片相关 **************/
|
||||||
|
const floorImage = ref("");
|
||||||
const getFloorImage = async () => {
|
const getFloorImage = async () => {
|
||||||
try {
|
try {
|
||||||
console.log(storeId.value);
|
console.log(storeId.value);
|
||||||
@@ -111,7 +106,6 @@ const getFloorImage = async () => {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const floorImage = ref("");
|
|
||||||
|
|
||||||
/************** 热力图相关 **************/
|
/************** 热力图相关 **************/
|
||||||
const heatInstance = ref(null);
|
const heatInstance = ref(null);
|
||||||
|
Reference in New Issue
Block a user