Commit 7f68aed8 by 陈岩

feat: 完成门店热力h5

1 parent 86878dfb
......@@ -6,8 +6,12 @@ const heatmap = {
},
// 获取热力图数据
getHeatMapValueAPi(params, config) {
return req("post", `/report/heatMap/get`, params, config);
getHeatMapValueApi(params, config) {
return req("post", `/report/heatMap/getNew`, params, config);
},
// 获取门店数据
getStoreDataApi(mallId) {
return req("get", `/report/b-mall/${mallId}`);
},
};
export default heatmap;
......@@ -34,7 +34,10 @@ cite,
code,
input,
select,
textarea{margin:0;padding:0;/* font-family:"fang"; */}
textarea {
margin: 0;
padding: 0; /* font-family:"fang"; */
}
h1,
h2,
h3,
......@@ -47,63 +50,174 @@ cite,
address,
sup,
sub,
th{font-weight:normal;font-style:normal;vertical-align:auto;font-size:1em;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-tap-highlight-color:transparent;}
i{font-style:normal;}
th {
font-weight: normal;
font-style: normal;
vertical-align: auto;
font-size: 1em;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-tap-highlight-color: transparent;
}
i {
font-style: normal;
}
/* @font-face{
font-family: fang;
src: url('http://h5.wufae.com/Chevrolet/upload/static/css/fang.ttf') format('truetype');
} */
*{/* font-family:"fang"; */box-sizing:border-box;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-tap-highlight-color:transparent;}
* {
/* font-family:"fang"; */
box-sizing: border-box;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-tap-highlight-color: transparent;
}
input,
textarea,
input:focus,
select:focus,
textarea:focus{/* font-family:"fang"; */border-width:0px;border-color:transparent;-webkit-tap-highlight-color:transparent;outline:transparent;}
textarea:focus {
/* font-family:"fang"; */
border-width: 0px;
border-color: transparent;
-webkit-tap-highlight-color: transparent;
outline: transparent;
}
ul,
ol{list-style-type:none;}
ol {
list-style-type: none;
}
a:link,
a:visited{text-decoration:none;outline:0 none;}
a:visited {
text-decoration: none;
outline: 0 none;
}
a:hover,
a:active{text-decoration:underline;outline:0 none;}
a:active {
text-decoration: underline;
outline: 0 none;
}
fieldset,
a img{border:none;}
img{vertical-align:top;display:block;}
a img {
border: none;
}
img {
vertical-align: top;
display: block;
}
input,
textarea,
button{font-size:100%;}
button{cursor:pointer;}
textarea{resize:none;overflow:auto;}
table{border-collapse:collapse;border-spacing:0;}
select optgroup{font-style:normal;}
legend{display:none;}
button {
font-size: 100%;
}
button {
cursor: pointer;
}
textarea {
resize: none;
overflow: auto;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
select optgroup {
font-style: normal;
}
legend {
display: none;
}
input[type="radio"],
input[type="checkbox"],
textarea{vertical-align:middle;}
textarea {
vertical-align: middle;
}
input,
select{background:transparent;border:0;outline:none;}
select {
background: transparent;
border: 0;
outline: none;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button{-webkit-appearance:none!important;margin:0;}
input::-webkit-inner-spin-button {
-webkit-appearance: none !important;
margin: 0;
}
.iswin input[type="radio"],
.iswin input[type="checkbox"],
.iswin textarea{vertical-align:-3px;}
.iswin textarea {
vertical-align: -3px;
}
input[type="radio"],
input[type="checkbox"]{margin-right:3px;}
input[type="checkbox"] {
margin-right: 3px;
}
input[type="text"],
input[type="password"],
textarea{border:none;}
textarea {
border: none;
}
input[type="text"]:focus,
input[type="password"]:focus,
a,
img{tap-highlight-color:rgba(0,0,0,0);focus-ring-color:rgba(0,0,0,0);-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-focus-ring-color:rgba(0,0,0,0);-moz-tap-highlight-color:rgba(0,0,0,0);-moz-focus-ring-color:rgba(0,0,0,0);}
.application{position:absolute;left:0;top:0;bottom:0;right:0;}
img {
tap-highlight-color: rgba(0, 0, 0, 0);
focus-ring-color: rgba(0, 0, 0, 0);
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-focus-ring-color: rgba(0, 0, 0, 0);
-moz-tap-highlight-color: rgba(0, 0, 0, 0);
-moz-focus-ring-color: rgba(0, 0, 0, 0);
}
.application {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
}
/*=====================================
ȫ?? =====================================*/
html{height:100%;overflow:hidden;}
body{transition:opacity 0.25s;position:relative;margin:0 auto;width:100%;height:100%;}
body.succ{opacity:1;}
.root_gap{position:relative; /* width:96%; */margin:0 30px;}
.clearfix{zoom:1;}
.clearfix:after{content:"";display:block;height:0;font-size:0;clear:both;overflow:hidden;visibility:hidden;}
.fullPage{position:absolute;left:0;top:0;width: 100%;height: 100%;background-color: #f3f9ff;}
.touchmove{overflow:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch;overflow-scrolling:touch;}
\ No newline at end of file
html {
height: 100%;
overflow: hidden;
}
body {
transition: opacity 0.25s;
position: relative;
margin: 0 auto;
width: 100%;
height: 100%;
}
body.succ {
opacity: 1;
}
.root_gap {
position: relative; /* width:96%; */
margin: 0 30px;
}
.clearfix {
zoom: 1;
}
.clearfix:after {
content: "";
display: block;
height: 0;
font-size: 0;
clear: both;
overflow: hidden;
visibility: hidden;
}
.fullPage {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: #fff;
}
.touchmove {
overflow: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
overflow-scrolling: touch;
}
<template>
<div>
<div class="heat-map" style="background-color: #fff">
<div class="canvas">
<img
:src="floorImage"
......@@ -15,9 +15,13 @@
></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"
v-if="channelList.length"
>
<van-slider
bar-height="20px"
bar-height="15px"
:max="sliderMax"
:min="1"
v-model="sliderVal"
......@@ -28,6 +32,8 @@
<script setup>
import { onMounted, watch, ref, nextTick } from "vue";
import { useRoute } from "vue-router";
import { Toast } from "vant";
import heatmap from "@/api/heatmap"; // 假设你的API文件路径
import { heatmapFactory } from "@/utils/heatmap.js";
let map = null; // 地图对象
......@@ -52,6 +58,8 @@ const getChannelList = async (mallId) => {
mallId,
channelList: channelList.value,
});
} else {
Toast.fail("无热力数据");
}
} else {
console.error("Failed to fetch channel list:", data.message);
......@@ -70,13 +78,13 @@ const getHeatMapData = async (params) => {
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",
counttime_gte: `${startDate.value} 00:00:00`,
counttime_lte: `${endDate.value} 23:59:59`,
countdate: endDate.value,
mallId: params.mallId,
};
const { data } = await heatmap.getHeatMapValueAPi(options);
const { data } = await heatmap.getHeatMapValueApi(options);
if (data.code === 200) {
heatDataObj.value = data.data || [];
nextTick(() => {
......@@ -90,14 +98,29 @@ const getHeatMapData = async (params) => {
}
};
const floorImage =
"https://store.keliuyun.com/images/report/mallPic/32a04280-5a32-4b0e-a2a5-15874da46a1120240826163115.jpg"; // 楼层图片
/************** 图片相关 **************/
const getFloorImage = async () => {
try {
console.log(storeId.value);
const { data } = await heatmap.getStoreDataApi(storeId.value);
if (data.code === 200) {
return data.data?.mallPlan || "";
}
return "";
} catch (error) {
return "";
}
};
const floorImage = ref("");
/************** 热力图相关 **************/
const heatInstance = ref(null);
const heatRadius = ref(10); // 热力图半径
const heatDataObj = ref([]); // 热力图数据对象
const timeLevel = ref("rt"); // 时间级别,默认实时 rt:停留时长 rc:顾客人次
const normalWidth = ref(100);
const startDate = ref(""); // 开始时间
const endDate = ref(""); // 结束时间
const interpolationCache = ref([]); // 插值缓存
const pointsData = ref([]); // 热力图点数据
......@@ -105,89 +128,95 @@ 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;
}
try {
const img = document.getElementById("editFloorimg");
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: [] });
heatInstance.value.destroy();
}
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;
}
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: [] });
}
if (isAdd) {
newPoints.push({
x: one.x,
y: one.y,
value: one.value,
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: () => {},
});
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;
});
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);
interpolationCache.value = [];
// let maxVal = Math.max(..._.pluck(points, "value")) * 5;
// maxVal = maxVal*10;
pointsData.value = newPoints;
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);
}
uni.postMessage({
type: "loadSuccess",
});
} catch (error) {
console.error("Error in dealHeatData:", error);
} finally {
Toast.clear();
}
}
function combinePoint(point, pointToler = 8) {
......@@ -199,7 +228,6 @@ function slideHandle(val) {
timeLevel.value == "rt" ? getTimeMin(val) : Math.round(val) + "";
heatInstance.value.setDataMax(sliderVal.value);
}
function getTimeMin(seconds) {
if (isNaN(seconds)) return seconds;
return (
......@@ -209,21 +237,37 @@ function getTimeMin(seconds) {
":00"
);
}
function numFormat(val) {
return val > 9 ? val : "0" + val;
}
watch(
() => $route.query,
(newVal) => {
const { token, mallId } = newVal;
async (newVal) => {
const { token, mallId, startDate: sDate, endDate: eDate, level } = 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) {
timeLevel.value = level || "rt"; // 默认实时
Toast.loading({
message: "加载中...",
forbidClick: true,
duration: 0,
});
storeId.value = mallId;
getChannelList(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 }
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!