Skip to content
Toggle navigation
Projects
Groups
Snippets
Help
Toggle navigation
This project
Loading...
Sign in
蒋秀川
/
miniProject
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit 644bca40
authored
May 30, 2025
by
陈岩
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
feat: 完成区域热力功能
1 parent
7f68aed8
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
363 additions
and
8 deletions
h5/src/api/heatMap.js
h5/src/router/index.js
h5/src/views/areaHeat/index.vue
h5/src/views/heatMap/index.vue
h5/src/api/heatMap.js
View file @
644bca4
...
@@ -13,5 +13,8 @@ const heatmap = {
...
@@ -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
;
h5/src/router/index.js
View file @
644bca4
...
@@ -12,6 +12,11 @@ const routes = [
...
@@ -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
({
...
...
h5/src/views/areaHeat/index.vue
0 → 100644
View file @
644bca4
<
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
>
h5/src/views/heatMap/index.vue
View file @
644bca4
...
@@ -7,14 +7,8 @@
...
@@ -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) => {
...
@@ -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 () => {
...
@@ -111,7 +106,6 @@ const getFloorImage = async () => {
return
""
;
return
""
;
}
}
};
};
const
floorImage
=
ref
(
""
);
/************** 热力图相关 **************/
/************** 热力图相关 **************/
const
heatInstance
=
ref
(
null
);
const
heatInstance
=
ref
(
null
);
...
...
Write
Preview
Markdown
is supported
Attach a file
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to post a comment