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 7f68aed8
authored
May 30, 2025
by
陈岩
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
feat: 完成门店热力h5
1 parent
86878dfb
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
286 additions
and
124 deletions
h5/src/api/heatMap.js
h5/src/static/css/index.css
h5/src/views/heatMap/index.vue
h5/src/api/heatMap.js
View file @
7f68aed
...
...
@@ -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
;
h5/src/static/css/index.css
View file @
7f68aed
...
...
@@ -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
;
}
h5/src/views/heatMap/index.vue
View file @
7f68aed
<
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=
"
20
px"
bar-height=
"
15
px"
: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
.
getHeatMapValueA
P
i
(
options
);
const
{
data
}
=
await
heatmap
.
getHeatMapValueA
p
i
(
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
}
...
...
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