Commit 7f68aed8 by 陈岩

feat: 完成门店热力h5

1 parent 86878dfb
...@@ -6,8 +6,12 @@ const heatmap = { ...@@ -6,8 +6,12 @@ const heatmap = {
}, },
// 获取热力图数据 // 获取热力图数据
getHeatMapValueAPi(params, config) { getHeatMapValueApi(params, config) {
return req("post", `/report/heatMap/get`, params, config); return req("post", `/report/heatMap/getNew`, params, config);
},
// 获取门店数据
getStoreDataApi(mallId) {
return req("get", `/report/b-mall/${mallId}`);
}, },
}; };
export default heatmap; export default heatmap;
...@@ -34,7 +34,10 @@ cite, ...@@ -34,7 +34,10 @@ cite,
code, code,
input, input,
select, select,
textarea{margin:0;padding:0;/* font-family:"fang"; */} textarea {
margin: 0;
padding: 0; /* font-family:"fang"; */
}
h1, h1,
h2, h2,
h3, h3,
...@@ -47,63 +50,174 @@ cite, ...@@ -47,63 +50,174 @@ cite,
address, address,
sup, sup,
sub, 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;} th {
i{font-style:normal;} 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-face{
font-family: fang; font-family: fang;
src: url('http://h5.wufae.com/Chevrolet/upload/static/css/fang.ttf') format('truetype'); 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, input,
textarea, textarea,
input:focus, input:focus,
select: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, ul,
ol{list-style-type:none;} ol {
list-style-type: none;
}
a:link, a:link,
a:visited{text-decoration:none;outline:0 none;} a:visited {
text-decoration: none;
outline: 0 none;
}
a:hover, a:hover,
a:active{text-decoration:underline;outline:0 none;} a:active {
text-decoration: underline;
outline: 0 none;
}
fieldset, fieldset,
a img{border:none;} a img {
img{vertical-align:top;display:block;} border: none;
}
img {
vertical-align: top;
display: block;
}
input, input,
textarea, textarea,
button{font-size:100%;} button {
button{cursor:pointer;} font-size: 100%;
textarea{resize:none;overflow:auto;} }
table{border-collapse:collapse;border-spacing:0;} button {
select optgroup{font-style:normal;} cursor: pointer;
legend{display:none;} }
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="radio"],
input[type="checkbox"], input[type="checkbox"],
textarea{vertical-align:middle;} textarea {
vertical-align: middle;
}
input, input,
select{background:transparent;border:0;outline:none;} select {
background: transparent;
border: 0;
outline: none;
}
input::-webkit-outer-spin-button, 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="radio"],
.iswin input[type="checkbox"], .iswin input[type="checkbox"],
.iswin textarea{vertical-align:-3px;} .iswin textarea {
vertical-align: -3px;
}
input[type="radio"], input[type="radio"],
input[type="checkbox"]{margin-right:3px;} input[type="checkbox"] {
margin-right: 3px;
}
input[type="text"], input[type="text"],
input[type="password"], input[type="password"],
textarea{border:none;} textarea {
border: none;
}
input[type="text"]:focus, input[type="text"]:focus,
input[type="password"]:focus, input[type="password"]:focus,
a, 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);} img {
.application{position:absolute;left:0;top:0;bottom:0;right:0;} 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 \ 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> <template>
<div> <div class="heat-map" style="background-color: #fff">
<div class="canvas"> <div class="canvas">
<img <img
:src="floorImage" :src="floorImage"
...@@ -15,9 +15,13 @@ ...@@ -15,9 +15,13 @@
></div> ></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> --> <!-- <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 <van-slider
bar-height="20px" bar-height="15px"
:max="sliderMax" :max="sliderMax"
:min="1" :min="1"
v-model="sliderVal" v-model="sliderVal"
...@@ -28,6 +32,8 @@ ...@@ -28,6 +32,8 @@
<script setup> <script setup>
import { onMounted, watch, ref, nextTick } from "vue"; import { onMounted, watch, ref, nextTick } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { Toast } from "vant";
import heatmap from "@/api/heatmap"; // 假设你的API文件路径 import heatmap from "@/api/heatmap"; // 假设你的API文件路径
import { heatmapFactory } from "@/utils/heatmap.js"; import { heatmapFactory } from "@/utils/heatmap.js";
let map = null; // 地图对象 let map = null; // 地图对象
...@@ -52,6 +58,8 @@ const getChannelList = async (mallId) => { ...@@ -52,6 +58,8 @@ const getChannelList = async (mallId) => {
mallId, mallId,
channelList: channelList.value, channelList: channelList.value,
}); });
} else {
Toast.fail("无热力数据");
} }
} else { } else {
console.error("Failed to fetch channel list:", data.message); console.error("Failed to fetch channel list:", data.message);
...@@ -70,13 +78,13 @@ const getHeatMapData = async (params) => { ...@@ -70,13 +78,13 @@ const getHeatMapData = async (params) => {
genders: [1, 0], genders: [1, 0],
ages: ["0-18", "19-35", "36-55", "56-100"], ages: ["0-18", "19-35", "36-55", "56-100"],
personTypes: [0], personTypes: [0],
counttime_gte: "2025-05-29 10:00:00", counttime_gte: `${startDate.value} 00:00:00`,
counttime_lte: "2025-05-29 22:00:00", counttime_lte: `${endDate.value} 23:59:59`,
countdate: "2025-05-29", countdate: endDate.value,
mallId: params.mallId, mallId: params.mallId,
}; };
const { data } = await heatmap.getHeatMapValueAPi(options); const { data } = await heatmap.getHeatMapValueApi(options);
if (data.code === 200) { if (data.code === 200) {
heatDataObj.value = data.data || []; heatDataObj.value = data.data || [];
nextTick(() => { nextTick(() => {
...@@ -90,14 +98,29 @@ const getHeatMapData = async (params) => { ...@@ -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 heatInstance = ref(null);
const heatRadius = ref(10); // 热力图半径 const heatRadius = ref(10); // 热力图半径
const heatDataObj = ref([]); // 热力图数据对象 const heatDataObj = ref([]); // 热力图数据对象
const timeLevel = ref("rt"); // 时间级别,默认实时 rt:停留时长 rc:顾客人次 const timeLevel = ref("rt"); // 时间级别,默认实时 rt:停留时长 rc:顾客人次
const normalWidth = ref(100); const normalWidth = ref(100);
const startDate = ref(""); // 开始时间
const endDate = ref(""); // 结束时间
const interpolationCache = ref([]); // 插值缓存 const interpolationCache = ref([]); // 插值缓存
const pointsData = ref([]); // 热力图点数据 const pointsData = ref([]); // 热力图点数据
...@@ -105,89 +128,95 @@ const sliderMax = ref(0); // 滑块最大值 ...@@ -105,89 +128,95 @@ const sliderMax = ref(0); // 滑块最大值
const sliderVal = ref(0); // 滑块当前值 const sliderVal = ref(0); // 滑块当前值
const marks = ref({}); // 滑块标记 const marks = ref({}); // 滑块标记
function dealHeatData() { function dealHeatData() {
const img = document.getElementById("editFloorimg"); try {
console.log(img.complete, img.naturalWidth); const img = document.getElementById("editFloorimg");
if (!img.complete || img.naturalWidth === 0) { if (!img.complete || img.naturalWidth === 0) {
// 等待图片加载完成 // 等待图片加载完成
img.onload = () => dealHeatData(); img.onload = () => dealHeatData();
return; 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) { if (heatInstance.value) {
heatInstance.value.setData({ data: [] }); heatInstance.value.destroy();
} }
let heatDom = document.getElementById("canvas-position"); let { width, height, naturalWidth, naturalHeight } =
heatDom.style.width = width + "px"; document.getElementById("editFloorimg");
heatDom.style.height = height + "px"; let radix = 0;
heatInstance.value = heatmapFactory.create({ if (naturalWidth === 0 || naturalHeight === 0) {
container: heatDom, return;
radius: heatRadius.value, }
onExtremaChange: function (data) { if (naturalWidth !== 0 && naturalHeight !== 0) {
console.log(data); radix = width / naturalWidth;
}, if (heatInstance.value) {
}); heatInstance.value.setData({ 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;
}
} }
if (isAdd) { let heatDom = document.getElementById("canvas-position");
newPoints.push({ heatDom.style.width = width + "px";
x: one.x, heatDom.style.height = height + "px";
y: one.y, heatInstance.value = heatmapFactory.create({
value: one.value, 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) => { newPoints.forEach((item, index) => {
maxVal = maxVal > item.value ? maxVal : item.value; maxVal = maxVal > item.value ? maxVal : item.value;
}); });
interpolationCache.value = []; interpolationCache.value = [];
// let maxVal = Math.max(..._.pluck(points, "value")) * 5; // let maxVal = Math.max(..._.pluck(points, "value")) * 5;
// maxVal = maxVal*10; // maxVal = maxVal*10;
pointsData.value = newPoints; pointsData.value = newPoints;
console.log(newPoints, "--s"); heatInstance.value.setData({ data: newPoints });
heatInstance.value.setData({ data: newPoints }); heatInstance.value.setDataMax(maxVal);
heatInstance.value.setDataMax(maxVal); let newMax =
let newMax = timeLevel.value == "rt"
timeLevel.value == "rt" ? (parseInt(maxVal / 3600) * 1 + 1) * 3600
? (parseInt(maxVal / 3600) * 1 + 1) * 3600 : maxVal;
: maxVal; let newVal = newMax / 4;
let newVal = newMax / 4; // 固定最大值好默认值
// 固定最大值好默认值 sliderMax.value = newMax;
sliderMax.value = newMax; sliderVal.value = newVal;
sliderVal.value = newVal; slideHandle(sliderVal.value);
slideHandle(sliderVal.value); }
uni.postMessage({
type: "loadSuccess",
});
} catch (error) {
console.error("Error in dealHeatData:", error);
} finally {
Toast.clear();
} }
} }
function combinePoint(point, pointToler = 8) { function combinePoint(point, pointToler = 8) {
...@@ -199,7 +228,6 @@ function slideHandle(val) { ...@@ -199,7 +228,6 @@ function slideHandle(val) {
timeLevel.value == "rt" ? getTimeMin(val) : Math.round(val) + ""; timeLevel.value == "rt" ? getTimeMin(val) : Math.round(val) + "";
heatInstance.value.setDataMax(sliderVal.value); heatInstance.value.setDataMax(sliderVal.value);
} }
function getTimeMin(seconds) { function getTimeMin(seconds) {
if (isNaN(seconds)) return seconds; if (isNaN(seconds)) return seconds;
return ( return (
...@@ -209,21 +237,37 @@ function getTimeMin(seconds) { ...@@ -209,21 +237,37 @@ function getTimeMin(seconds) {
":00" ":00"
); );
} }
function numFormat(val) { function numFormat(val) {
return val > 9 ? val : "0" + val; return val > 9 ? val : "0" + val;
} }
watch( watch(
() => $route.query, () => $route.query,
(newVal) => { async (newVal) => {
const { token, mallId } = newVal; const { token, mallId, startDate: sDate, endDate: eDate, level } = newVal;
if (token) { if (token) {
window.localStorage.setItem("atoken", 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) { if (mallId) {
timeLevel.value = level || "rt"; // 默认实时
Toast.loading({
message: "加载中...",
forbidClick: true,
duration: 0,
});
storeId.value = mallId; 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 } { immediate: true }
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!