Commit 86878dfb by 陈岩

feat: 增加热力图

1 parent 685b0a49
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,viewport-fit=cover,initial-scale=1.0,maximum-scale=1.0, user-scalable=no">
<title>视频巡店</title>
<script src="/jessibuca-pro/jessibuca-pro-demo.js"></script>
<script src="/jessibuca-pro/jessibuca-pro-talk-demo.js"></script>
</head>
<body>
<div id="app" class="fullPage"></div>
<script type="module" src="/src/main.js"></script>
</body>
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width,viewport-fit=cover,initial-scale=1.0,maximum-scale=1.0, user-scalable=no"
/>
<title>视频巡店</title>
<script src="/jessibuca-pro/jessibuca-pro-demo.js"></script>
<script src="/jessibuca-pro/jessibuca-pro-talk-demo.js"></script>
<script src="/jessibuca-pro/uni.webview.1.5.5.js"></script>
</head>
<body>
<div id="app" class="fullPage"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
......@@ -10,14 +10,12 @@
"axios": "^0.26.1",
"blueimp-md5": "^2.16.0",
"echarts": "^5.3.2",
<<<<<<< HEAD
"ezuikit-js": "^7.7.2",
=======
"ezuikit-js": "^7.7.0",
"fengmap": "^3.1.5",
"heatmap.js": "2.0.5",
"jquery": "^3.7.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
>>>>>>> c16b0fd5ce3372f783957d2726c759038451ac92
"postcss-px-to-viewport": "^1.1.1",
"rollup-plugin-copy": "^3.4.0",
"sass": "^1.32.7",
......@@ -26,6 +24,7 @@
"vant": "^3.6.12",
"vconsole": "^3.14.6",
"vue": "^3.0.6",
"vue-router": "^4.5.1",
"weixin-js-sdk": "^1.6.0"
},
"devDependencies": {
......
This diff could not be displayed because it is too large.
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e=e||self).uni=n()}(this,(function(){"use strict";try{var e={};Object.defineProperty(e,"passive",{get:function(){!0}}),window.addEventListener("test-passive",null,e)}catch(e){}var n=Object.prototype.hasOwnProperty;function i(e,i){return n.call(e,i)}var t=[];function o(){return window.__dcloud_weex_postMessage||window.__dcloud_weex_}function a(){return window.__uniapp_x_postMessage||window.__uniapp_x_}var r=function(e,n){var i={options:{timestamp:+new Date},name:e,arg:n};if(a()){if("postMessage"===e){var r={data:n};return window.__uniapp_x_postMessage?window.__uniapp_x_postMessage(r):window.__uniapp_x_.postMessage(JSON.stringify(r))}var d={type:"WEB_INVOKE_APPSERVICE",args:{data:i,webviewIds:t}};window.__uniapp_x_postMessage?window.__uniapp_x_postMessageToService(d):window.__uniapp_x_.postMessageToService(JSON.stringify(d))}else if(o()){if("postMessage"===e){var s={data:[n]};return window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessage(s):window.__dcloud_weex_.postMessage(JSON.stringify(s))}var w={type:"WEB_INVOKE_APPSERVICE",args:{data:i,webviewIds:t}};window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessageToService(w):window.__dcloud_weex_.postMessageToService(JSON.stringify(w))}else{if(!window.plus)return window.parent.postMessage({type:"WEB_INVOKE_APPSERVICE",data:i,pageId:""},"*");if(0===t.length){var u=plus.webview.currentWebview();if(!u)throw new Error("plus.webview.currentWebview() is undefined");var g=u.parent(),v="";v=g?g.id:u.id,t.push(v)}if(plus.webview.getWebviewById("__uniapp__service"))plus.webview.postMessageToUniNView({type:"WEB_INVOKE_APPSERVICE",args:{data:i,webviewIds:t}},"__uniapp__service");else{var c=JSON.stringify(i);plus.webview.getLaunchWebview().evalJS('UniPlusBridge.subscribeHandler("'.concat("WEB_INVOKE_APPSERVICE",'",').concat(c,",").concat(JSON.stringify(t),");"))}}},d={navigateTo:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;r("navigateTo",{url:encodeURI(n)})},navigateBack:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.delta;r("navigateBack",{delta:parseInt(n)||1})},switchTab:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;r("switchTab",{url:encodeURI(n)})},reLaunch:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;r("reLaunch",{url:encodeURI(n)})},redirectTo:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;r("redirectTo",{url:encodeURI(n)})},getEnv:function(e){a()?e({uvue:!0}):o()?e({nvue:!0}):window.plus?e({plus:!0}):e({h5:!0})},postMessage:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};r("postMessage",e.data||{})}},s=/uni-app/i.test(navigator.userAgent),w=/Html5Plus/i.test(navigator.userAgent),u=/complete|loaded|interactive/;var g=window.my&&navigator.userAgent.indexOf(["t","n","e","i","l","C","y","a","p","i","l","A"].reverse().join(""))>-1;var v=window.swan&&window.swan.webView&&/swan/i.test(navigator.userAgent);var c=window.qq&&window.qq.miniProgram&&/QQ/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var p=window.tt&&window.tt.miniProgram&&/toutiaomicroapp/i.test(navigator.userAgent);var _=window.wx&&window.wx.miniProgram&&/micromessenger/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var m=window.qa&&/quickapp/i.test(navigator.userAgent);var f=window.ks&&window.ks.miniProgram&&/micromessenger/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var l=window.tt&&window.tt.miniProgram&&/Lark|Feishu/i.test(navigator.userAgent);var E=window.jd&&window.jd.miniProgram&&/micromessenger/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var x=window.xhs&&window.xhs.miniProgram&&/xhsminiapp/i.test(navigator.userAgent);for(var S,h=function(){window.UniAppJSBridge=!0,document.dispatchEvent(new CustomEvent("UniAppJSBridgeReady",{bubbles:!0,cancelable:!0}))},y=[function(e){if(s||w)return window.__uniapp_x_postMessage||window.__uniapp_x_||window.__dcloud_weex_postMessage||window.__dcloud_weex_?document.addEventListener("DOMContentLoaded",e):window.plus&&u.test(document.readyState)?setTimeout(e,0):document.addEventListener("plusready",e),d},function(e){if(_)return window.WeixinJSBridge&&window.WeixinJSBridge.invoke?setTimeout(e,0):document.addEventListener("WeixinJSBridgeReady",e),window.wx.miniProgram},function(e){if(c)return window.QQJSBridge&&window.QQJSBridge.invoke?setTimeout(e,0):document.addEventListener("QQJSBridgeReady",e),window.qq.miniProgram},function(e){if(g){document.addEventListener("DOMContentLoaded",e);var n=window.my;return{navigateTo:n.navigateTo,navigateBack:n.navigateBack,switchTab:n.switchTab,reLaunch:n.reLaunch,redirectTo:n.redirectTo,postMessage:n.postMessage,getEnv:n.getEnv}}},function(e){if(v)return document.addEventListener("DOMContentLoaded",e),window.swan.webView},function(e){if(p)return document.addEventListener("DOMContentLoaded",e),window.tt.miniProgram},function(e){if(m){window.QaJSBridge&&window.QaJSBridge.invoke?setTimeout(e,0):document.addEventListener("QaJSBridgeReady",e);var n=window.qa;return{navigateTo:n.navigateTo,navigateBack:n.navigateBack,switchTab:n.switchTab,reLaunch:n.reLaunch,redirectTo:n.redirectTo,postMessage:n.postMessage,getEnv:n.getEnv}}},function(e){if(f)return window.WeixinJSBridge&&window.WeixinJSBridge.invoke?setTimeout(e,0):document.addEventListener("WeixinJSBridgeReady",e),window.ks.miniProgram},function(e){if(l)return document.addEventListener("DOMContentLoaded",e),window.tt.miniProgram},function(e){if(E)return window.JDJSBridgeReady&&window.JDJSBridgeReady.invoke?setTimeout(e,0):document.addEventListener("JDJSBridgeReady",e),window.jd.miniProgram},function(e){if(x)return window.xhs.miniProgram},function(e){return document.addEventListener("DOMContentLoaded",e),d}],M=0;M<y.length&&!(S=y[M](h));M++);S||(S={});var P="undefined"!=typeof uni?uni:{};if(!P.navigateTo)for(var b in S)i(S,b)&&(P[b]=S[b]);return P.webView=S,P}));
import req from "@/api/http.js";
const heatmap = {
//获取设备通道
getChannelsListApi(params, config) {
return req("get", `/report/channels`, params, config);
},
// 获取热力图数据
getHeatMapValueAPi(params, config) {
return req("post", `/report/heatMap/get`, params, config);
},
};
export default heatmap;
import { createApp } from 'vue';
import wx from 'weixin-js-sdk';
import { createApp } from "vue";
import wx from "weixin-js-sdk";
window.wx = wx;
import App from './App.vue';
import Vant from 'vant';
import '@s/css/index.css';
import 'vant/lib/index.css';
import router from "./router";
import App from "./App.vue";
import Vant from "vant";
import "@s/css/index.css";
import "vant/lib/index.css";
const app = createApp(App);
app.use(Vant);
app.mount('#app')
app.use(router);
app.mount("#app");
import { createRouter, createWebHistory } from "vue-router";
// 示例路由配置
const routes = [
{
path: "/",
name: "Video",
component: () => import("@/views/video/index.vue"),
},
{
path: "/heatMap",
name: "HeatMap",
component: () => import("@/views/heatMap/index.vue"),
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
<template>
<div>
<div class="canvas">
<img
:src="floorImage"
class="editFloorimg"
id="editFloorimg"
style="width: 100%"
/>
<div
class="canvas-position"
id="canvas-position"
@mousemove="mousemoveHandle"
@mouseout="mouseoutHandle"
></div>
</div>
<!-- <el-slider v-model="sliderVal" :marks="marks" :format-tooltip="formatTooltip" :max="sliderMax" :min="1" vertical @change="slideHandle(sliderVal)" height="200px"></el-slider> -->
<van-slider
bar-height="20px"
:max="sliderMax"
:min="1"
v-model="sliderVal"
@change="slideHandle"
/>
</div>
</template>
<script setup>
import { onMounted, watch, ref, nextTick } from "vue";
import { useRoute } from "vue-router";
import heatmap from "@/api/heatmap"; // 假设你的API文件路径
import { heatmapFactory } from "@/utils/heatmap.js";
let map = null; // 地图对象
const $route = useRoute();
// 监听route参数
// ?atoken='xxx'&mallId='xxx'
const storeId = ref(""); // 店铺id
const channelList = ref([]); // 渠道列表
const getChannelList = async (mallId) => {
try {
const { data } = await heatmap.getChannelsListApi({ mallId, status: 1 });
if (data.code === 200) {
channelList.value =
data.data?.map((item) => {
return item.serialnum;
}) || [];
if (channelList.value.length) {
getHeatMapData({
mallId,
channelList: channelList.value,
});
}
} else {
console.error("Failed to fetch channel list:", data.message);
channelList.value = [];
}
} catch (error) {
console.error("Error fetching channel list:", error);
}
};
// 获取热力图数据
const getHeatMapData = async (params) => {
try {
const options = {
channelSerialnum_arr: params.channelList,
type: 2,
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",
mallId: params.mallId,
};
const { data } = await heatmap.getHeatMapValueAPi(options);
if (data.code === 200) {
heatDataObj.value = data.data || [];
nextTick(() => {
dealHeatData();
});
} else {
}
} catch (error) {
console.error("Error fetching heat map data:", error);
return [];
}
};
const floorImage =
"https://store.keliuyun.com/images/report/mallPic/32a04280-5a32-4b0e-a2a5-15874da46a1120240826163115.jpg"; // 楼层图片
const heatInstance = ref(null);
const heatRadius = ref(10); // 热力图半径
const heatDataObj = ref([]); // 热力图数据对象
const timeLevel = ref("rt"); // 时间级别,默认实时 rt:停留时长 rc:顾客人次
const normalWidth = ref(100);
const interpolationCache = ref([]); // 插值缓存
const pointsData = ref([]); // 热力图点数据
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;
}
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: [] });
}
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;
}
}
if (isAdd) {
newPoints.push({
x: one.x,
y: one.y,
value: one.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);
}
}
function combinePoint(point, pointToler = 8) {
return parseInt(point - (point % pointToler));
}
function slideHandle(val) {
marks.value = {};
marks.value[val] =
timeLevel.value == "rt" ? getTimeMin(val) : Math.round(val) + "";
heatInstance.value.setDataMax(sliderVal.value);
}
function getTimeMin(seconds) {
if (isNaN(seconds)) return seconds;
return (
numFormat(parseInt(seconds / 3600)) +
":" +
numFormat(parseInt((seconds % 3600) / 60)) +
":00"
);
}
function numFormat(val) {
return val > 9 ? val : "0" + val;
}
watch(
() => $route.query,
(newVal) => {
const { token, mallId } = newVal;
if (token) {
window.localStorage.setItem("atoken", token);
}
if (mallId) {
storeId.value = mallId;
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;
}
</style>
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import pxtovw from 'postcss-px-to-viewport'
import styleImport, { VantResolve } from 'vite-plugin-style-import';
const loder_pxtovw=pxtovw({
unitToConvert: "px", // 需要转换的单位,默认为"px"
viewportWidth: 750, // 设计稿的视口宽度
unitPrecision: 2, // 单位转换后保留的精度
propList: ["*"], // 能转化为vw的属性列表
viewportUnit: "vw", // 希望使用的视口单位
fontViewportUnit: "vw", // 字体使用的视口单位
selectorBlackList: ['ignore'], // 需要忽略的CSS选择器
exclude:[/node_module/],
})
import path from 'path';
import copy from 'rollup-plugin-copy'
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import pxtovw from "postcss-px-to-viewport";
import styleImport, { VantResolve } from "vite-plugin-style-import";
const loder_pxtovw = pxtovw({
unitToConvert: "px", // 需要转换的单位,默认为"px"
viewportWidth: 750, // 设计稿的视口宽度
unitPrecision: 2, // 单位转换后保留的精度
propList: ["*"], // 能转化为vw的属性列表
viewportUnit: "vw", // 希望使用的视口单位
fontViewportUnit: "vw", // 字体使用的视口单位
selectorBlackList: ["ignore"], // 需要忽略的CSS选择器
exclude: [/node_module/],
});
import path from "path";
import copy from "rollup-plugin-copy";
export default defineConfig({
base:'./',
publicDir:'public',
devServer: {
port: 9091,
proxy: {
'/nvsthird/ptzcontrol': {
target: 'http://52.130.155.147:8888',
changeOrigin: true
}
}
},
base: "./",
publicDir: "public",
server: {
host: true,
},
devServer: {
port: 9091,
proxy: {
"/nvsthird/ptzcontrol": {
target: "http://52.130.155.147:8888",
changeOrigin: true,
},
},
},
plugins: [
vue(),
vueJsx(),
styleImport({
libs: [
{
libraryName: "vant",
esModule: true,
resolveStyle: (name) => `vant/es/${name}/style`,
},
],
}),
vue(),
vueJsx(),
styleImport({
libs: [
{
libraryName: "vant",
esModule: true,
resolveStyle: (name) => `vant/es/${name}/style`,
},
],
}),
],
css: {
postcss:{
plugins: [loder_pxtovw]
}
postcss: {
plugins: [loder_pxtovw],
},
},
resolve:{
alias:{
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
"@s": path.resolve(__dirname, "./src/static")
}
"@s": path.resolve(__dirname, "./src/static"),
},
},
build: {
assetsDir: "static",
// rollupOptions:{
// plugins:[copy({
// targets: [
// { src: 'public/*', dest: 'dist/static' }
// ]
// })]
// }
},
build:{
assetsDir:'static',
// rollupOptions:{
// plugins:[copy({
// targets: [
// { src: 'public/*', dest: 'dist/static' }
// ]
// })]
// }
}
})
});
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!