聚合时根据数量展示不同图标。非聚合时根据状态展示不同图标。点击点位展示弹层,点击其他区域可关闭
index.html(key替换成自己的)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="/logo.png">
<link rel="dns-prefetch" href="//api.tianditu.gov.cn">
<!-- <link rel="icon" href="/favicon.ico">-->
<title>江南水务智慧化管控平台</title>
<script type="text/javascript">
window._AMapSecurityConfig = {
securityJsCode: "替换",
};
</script>
<!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
<script src="https://a.amap.com/jsapi_demos/static/demo-center/js/demoutils.js"></script>
<script src="https://a.amap.com/jsapi_demos/static/china.js"></script>
<script src="https://webapi.amap.com/maps?v=2.0&key=替换&plugin=AMap.MarkerCluster,AMap.IndexCluster,AMap.GeoJSON,AMap.Geocoder,AMap.Scale,AMap.ToolBar,AMap.DistrictSearch"></script>
<script src="https://webapi.amap.com/loca?v=2.0.0&key=替换"></script>
<script src="https://webapi.amap.com/ui/1.1/main.js?v=1.1.1"></script>
<style>
html,
body,
#app {
height: 100%;
margin: 0px;
padding: 0px;
}
.chromeframe {
margin: 0.2em 0;
background: #ccc;
color: #000;
padding: 0.2em 0;
}
#loader-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 999999;
}
#loader {
display: block;
position: relative;
left: 50%;
top: 50%;
width: 150px;
height: 150px;
margin: -75px 0 0 -75px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #FFF;
-webkit-animation: spin 2s linear infinite;
-ms-animation: spin 2s linear infinite;
-moz-animation: spin 2s linear infinite;
-o-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
z-index: 1001;
}
#loader:before {
content: "";
position: absolute;
top: 5px;
left: 5px;
right: 5px;
bottom: 5px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #FFF;
-webkit-animation: spin 3s linear infinite;
-moz-animation: spin 3s linear infinite;
-o-animation: spin 3s linear infinite;
-ms-animation: spin 3s linear infinite;
animation: spin 3s linear infinite;
}
#loader:after {
content: "";
position: absolute;
top: 15px;
left: 15px;
right: 15px;
bottom: 15px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #FFF;
-moz-animation: spin 1.5s linear infinite;
-o-animation: spin 1.5s linear infinite;
-ms-animation: spin 1.5s linear infinite;
-webkit-animation: spin 1.5s linear infinite;
animation: spin 1.5s linear infinite;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes spin {
0% {
-webkit-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
-ms-transform: rotate(360deg);
transform: rotate(360deg);
}
}
#loader-wrapper .loader-section {
position: fixed;
top: 0;
width: 51%;
height: 100%;
background: #7171C6;
z-index: 1000;
-webkit-transform: translateX(0);
-ms-transform: translateX(0);
transform: translateX(0);
}
#loader-wrapper .loader-section.section-left {
left: 0;
}
#loader-wrapper .loader-section.section-right {
right: 0;
}
.loaded #loader-wrapper .loader-section.section-left {
-webkit-transform: translateX(-100%);
-ms-transform: translateX(-100%);
transform: translateX(-100%);
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
}
.loaded #loader-wrapper .loader-section.section-right {
-webkit-transform: translateX(100%);
-ms-transform: translateX(100%);
transform: translateX(100%);
-webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
}
.loaded #loader {
opacity: 0;
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.loaded #loader-wrapper {
visibility: hidden;
-webkit-transform: translateY(-100%);
-ms-transform: translateY(-100%);
transform: translateY(-100%);
-webkit-transition: all 0.3s 1s ease-out;
transition: all 0.3s 1s ease-out;
}
.no-js #loader-wrapper {
display: none;
}
.no-js h1 {
color: #222222;
}
#loader-wrapper .load_title {
font-family: 'Open Sans';
color: #FFF;
font-size: 19px;
width: 100%;
text-align: center;
z-index: 9999999999999;
position: absolute;
top: 60%;
opacity: 1;
line-height: 30px;
}
#loader-wrapper .load_title span {
font-weight: normal;
font-style: italic;
font-size: 13px;
color: #FFF;
opacity: 0.5;
}
</style>
</head>
<body>
<div id="app">
<div id="loader-wrapper">
<div id="loader"></div>
<div class="loader-section section-left"></div>
<div class="loader-section section-right"></div>
<div class="load_title">正在加载系统资源,请耐心等待</div>
</div>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
<template>
<div class="tiandi-map-container">
<!-- 地图区域-->
<div v-loading="map_loading" element-loading-text="地图加载中"
:element-loading-spinner="svg"
element-loading-svg-view-box="-10, -10, 50, 50"
element-loading-background="#001431" style="width:100%;height: 100%;position: relative;">
<div
style="width:100%;
height: 100%;
border: none;
box-sizing: border-box;
position: relative;"
ref="map_container"
id="map_container"
>
</div>
</div>
</div>
</template>
<script setup>
import {getInfoList} from "../../../api/screen/project";
const map_container = ref(null)
const curMap = ref();
const curAMap = ref(null) // 高德地图引用
const map_loading = ref(true)
const selectedInfo = ref({});
const infoWindow = ref(null)
const dataList = ref([])
const { proxy } = getCurrentInstance();
const { pm_project_period } = proxy.useDict("pm_project_period");
const svg = `
<path class="path" d="
M 30 15
L 28 17
M 25.61 25.61
A 15 15, 0, 0, 1, 15 30
A 15 15, 0, 1, 1, 27.99 7.5
L 15 15
" style="stroke-width: 4px; fill: rgba(0, 0, 0, 0)"/>
`
//地图及边界
const load_map = async () => {
map_loading.value = true
curMap.value = null
let query = {
pageSize:1000,
pageNum:1
}
let res = await getInfoList(query)
dataList.value = res.rows;
const district = new AMap.DistrictSearch({ subdistrict: 0, extensions: 'all', level: 'province' });
// 设置江阴市范围
district.search('江阴市', function (status, result) {
// 查询成功时,result即为对应的行政区信息
// 这里是整个江阴市的边界经纬度
const bounds = result.districtList[0].boundaries
const mask = []
for (let i = 0; i < bounds.length; i++) {
mask.push([bounds[i]])
}
curAMap.value = new AMap.Map("map_container", { // 设置地图容器id
mask: mask, // 为Map实例制定掩模的路径,各图层将值显示路径范围内图像,3D模式下有效
zoom: 11.6, // 设置当前显示级别
expandZoomRange: true, // 开启显示范围设置
zooms: [11.6, 20], //最小显示级别为7,最大显示级别为20
center: [120.284794, 31.841642], // 设置地图中心点位置 暂定江南水务总部
viewMode: "3D", // 特别注意,设置为3D则其他地区不显示
zoomEnable: true, // 是否可以缩放地图
resizeEnable: true,
mapStyle: "amap://styles/darkblue",
layers: [
// 卫星
// new AMap.TileLayer.Satellite(),
// 路网
// new AMap.TileLayer.RoadNet()
]
});
// 添加描边
for (let i = 0; i < bounds.length; i++) {
const polyline = new AMap.Polyline({
path: bounds[i], // polyline 路径,支持 lineString 和 MultiLineString
strokeColor: '#3078AC', // 线条颜色,使用16进制颜色代码赋值。默认值为#00D3FC
strokeWeight: 2, // 轮廓线宽度,默认为:2
// map:map // 这种方式相当于: polyline.setMap(map);
})
polyline.setMap(curAMap.value);
}
curMap.value = curAMap.value
// ★ 1. 点击空白处关闭 InfoWindow
curMap.value.on('click', closeInfoWindow)
//点标记
makeMarkers(dataList.value);
})
}
// ★ 2. 关闭 InfoWindow(如果已打开)
function closeInfoWindow() {
if (infoWindow.value) {
infoWindow.value.close()
infoWindow.value = null
}
}
//点位
function makeMarkers(devices) {
// 添加标记
//橙色 准备阶段 1
let rowsOrange = []
//绿色 已完成 5
let rowsGreen = []
//灰色 已关闭 4
let rowsGray = []
//红色 施工阶段 2
let rowsRed = []
//蓝色 完工阶段 3
let rowsBlue = []
var points = []
let rank = 1
for (let i = 0; i < devices.length; i++) {
if (devices[i].latLng) {
let latLng = devices[i].latLng.split(',')
let info = {
latLng: latLng,
flowRecordId: devices[i].flowRecordId,
projectName: devices[i].projectName,
projectCode: devices[i].projectCode,
projectType: devices[i].projectType,
flowId: devices[i].flowId,
projectPeriod: devices[i].projectPeriod,
projectManager: devices[i].projectManager,
projectId: devices[i].projectId,
}
//所有点
points.push({
weight: 1,
name: devices[i].flowRecordId,
lnglat: latLng,
info: info
})
if (devices[i].projectPeriod === '1') {
rowsOrange.push(info)
} else if (devices[i].projectPeriod === '2') {
rowsRed.push(info)
} else if (devices[i].projectPeriod === '3') {
rowsBlue.push(info)
} else if (devices[i].projectPeriod === '4') {
rowsGray.push(info)
} else if (devices[i].projectPeriod === '5') {
rowsGreen.push(info)
}
rank++
}
}
//marker点位做点击事件
var positions = []
for (let i = 0; i < rowsGreen.length; i++) {
let latLng = rowsGreen[i].latLng
positions.push({
lng: latLng[0],
lat: latLng[1],
info: rowsGreen[i],
type: 'green'
})
}
for (let i = 0; i < rowsGray.length; i++) {
let latLng = rowsGray[i].latLng
positions.push({
lng: latLng[0],
lat: latLng[1],
info: rowsGray[i],
type: 'gray'
})
}
for (let i = 0; i < rowsRed.length; i++) {
let latLng = rowsRed[i].latLng
positions.push({
lng: latLng[0],
lat: latLng[1],
info: rowsRed[i],
type: 'red'
})
}
for (let i = 0; i < rowsOrange.length; i++) {
let latLng = rowsOrange[i].latLng
positions.push({
lng: latLng[0],
lat: latLng[1],
info: rowsOrange[i],
type: 'orange'
})
}
for (let i = 0; i < rowsBlue.length; i++) {
let latLng = rowsBlue[i].latLng
positions.push({
lng: latLng[0],
lat: latLng[1],
info: rowsBlue[i],
type: 'blue'
})
}
handlerMarkerCluster(points)
setTimeout(() => {
map_loading.value = false
}, 500);
}
function deviceStatusMakeColor(status) {
//灰色 已关闭 4
if ([ '4', 4].includes(status)) {
return '#909399'
//绿色 已完成 5
} else if (['5', 5].includes(status)) {
return '#67C23A'
//红色 施工阶段 2
} else if (['2', 2].includes(status)) {
return '#F56C6C'
//橙色 准备阶段 1
} else if (['1', 1].includes(status)) {
return 'orange'
// 蓝色 完工阶段 3
} else if (['3', 3].includes(status)) {
return 'blue'
}
}
function getProjectType(value) {
const dict = pm_project_period.value;
for (let i = 0; i < dict.length; i++) {
if (dict[i].value == value) {
return dict[i].label
}
}
return ''
}
function statusIcon(status){
//灰色 已关闭 4
if ([ '4', 4].includes(status)) {
return marker04Img
//绿色 已完成 5
} else if (['5', 5].includes(status)) {
return marker05Img
//红色 施工阶段 2
} else if (['2', 2].includes(status)) {
return marker02Img
//橙色 准备阶段 1
} else if (['1', 1].includes(status)) {
return marker01Img
// 蓝色 完工阶段 3
} else if (['3', 3].includes(status)) {
return marker03Img
}
}
//点聚合逻辑
const handlerMarkerCluster = (points) => {
curAMap.value = new AMap.MarkerCluster(
curMap.value,
points,
{
gridSize: 60,
renderClusterMarker: handlerRenderClusterMarker, // 自定义聚合点样式
renderMarker: handlerRenderMarker // 自定义点样式
}
);
// 聚合点点击事件
curAMap.value.on('click', (e) => {
const { clusterData, marker } = e
// 单个点
const innerText = marker.dom.innerText
if (!innerText) return
// 计算聚合点的中心点
let [allLng, allLat] = [0, 0]
clusterData.forEach(item => {
const { lng, lat } = item.lnglat
allLng += lng
allLat += lat
})
const lngCenter = allLng / clusterData.length
const latCenter = allLat / clusterData.length
// 动态设置缩放级别
const curZoom = curMap.value.getZoom()
let targetZoom = curZoom + 3
curMap.value.setZoomAndCenter(
targetZoom,
[lngCenter, latCenter],
true
)
})
}
// 自定义聚合标记
const cMarker01 = new URL(`@/assets/images/project/blue.png`, import.meta.url).href;
const cMarker02 = new URL(`@/assets/images/project/green.png`, import.meta.url).href;
const cMarker03 = new URL(`@/assets/images/project/orange.png`, import.meta.url).href;
const cMarker04 = new URL(`@/assets/images/project/red.png`, import.meta.url).href;
const cMarker05 = new URL(`@/assets/images/project/darkRed.png`, import.meta.url).href;
const handlerRenderClusterMarker = (context) => {
const { count } = context
// 图标映射
let curCMarker, size
if (count <= 10) {
curCMarker = cMarker01
size = 48 // 原来 32 → 48
} else if (count <= 100) {
curCMarker = cMarker02
size = 48
} else if (count <= 1000) {
curCMarker = cMarker03
size = 56 // 原来 36 → 56
} else if (count <= 10000) {
curCMarker = cMarker04
size = 64 // 原来 48 → 64
} else {
curCMarker = cMarker05
size = 64
}
// 数字样式:白色加粗 + 文字阴影,保证深浅背景都能看清
const html = `
<div class="custom-cmarker"
style="width:${size}px;height:${size}px;
background-image:url(${curCMarker});
background-size:100% 100%;
display:flex;align-items:center;justify-content:center;">
<span style="color:#fff;font-size:16px;font-weight:bold;
text-shadow:0 0 2px #000;">${count}</span>
</div>`
context.marker.setContent(html)
}
function createContent() {
const container = document.createElement("div");
container.className = "info-window";
const header = document.createElement("div");
header.className = "info-header";
header.textContent = '工单编号:'+selectedInfo.value.flowRecordId;
const closeButton = document.createElement("span");
closeButton.className = "close-btn";
closeButton.textContent = "×";
closeButton.onclick = () => infoWindow.value.close();
header.appendChild(closeButton);
const body = document.createElement("div");
body.className = "info-body";
// Status row
const statusRow = document.createElement("div");
statusRow.className = "status-row";
const statusDot = document.createElement("span");
const deviceColor = deviceStatusMakeColor(selectedInfo.value.projectPeriod);
statusDot.className = "status-dot";
statusDot.style.backgroundColor = deviceColor;
const statusText = document.createElement("span");
// statusText.style.color = deviceColor;
statusText.textContent = getProjectType(selectedInfo.value.projectPeriod);
statusRow.appendChild(statusDot);
statusRow.appendChild(statusText);
body.appendChild(statusRow);
// Info rows
const infoItems = [
// { label: '工单编号', value: selectedInfo.value.flowRecordId },
{ label: '项目名称', value: selectedInfo.value.projectName },
{ label: '项目编号', value: selectedInfo.value.projectCode },
{ label: '项目类型', value: selectedInfo.value.projectType },
{ label: '工单类型', value: selectedInfo.value.flowId },
{ label: '项目阶段', value: getProjectType(selectedInfo.value.projectPeriod) },
{ label: '项目负责人', value: selectedInfo.value.projectManager },
{ label: '经纬度', value: selectedInfo.value.latLng },
];
infoItems.forEach(item => {
const row = document.createElement("div");
row.className = "info-row";
const label = document.createElement("span");
label.className = "info-label";
label.textContent = item.label + ":";
const value = document.createElement("span");
value.className = "info-value";
value.textContent = item.value;
row.appendChild(label);
row.appendChild(value);
body.appendChild(row);
});
container.appendChild(header);
container.appendChild(body);
return container;
}
// 非聚合状态
const marker03Img = new URL(`@/assets/images/project/position.png`, import.meta.url).href;
const marker04Img = new URL(`@/assets/images/project/position_down.png`, import.meta.url).href;
const marker05Img = new URL(`@/assets/images/project/position_green.png`, import.meta.url).href;
const marker01Img = new URL(`@/assets/images/project/position_warning.png`, import.meta.url).href;
const marker02Img = new URL(`@/assets/images/project/position_error.png`, import.meta.url).href;
const handlerRenderMarker = (context) => {
const { projectPeriod } = context.data[0].info
// 图标
const iconSrc = statusIcon(projectPeriod)
context.marker.setContent(
`<img class="project-custom-marker-img" src="${iconSrc}" />`)
// 关键:点击打开与原来样式一致的 InfoWindow
context.marker.on('click', async (e) => {
e.cancelBubble = true
selectedInfo.value = context.data[0].info
// 1. 放大并居中
const pos = e.target.getPosition()
curMap.value.setZoomAndCenter(20, [pos.lng, pos.lat], true)
// 2. 弹窗
infoWindow.value = new AMap.InfoWindow({
isCustom: true,
autoMove: true,
offset: { x:40, y: -30 }
})
infoWindow.value.setContent(createContent()) // 复用原函数
setTimeout(() => {
infoWindow.value.open(curMap.value, [pos.lng, pos.lat])
}, 100)
})
}
onMounted(() => {
// 注入样式
const style = document.createElement('style')
style.textContent = `
.info-window {
width: 600px;
max-width: 90vw;
height: auto;
max-height: 80vh;
padding: 0;
background: #1e1e1e;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.6);
overflow: hidden;
font-family: "Segoe UI", sans-serif;
color: #f0f0f0;
display: flex;
flex-direction: column;
}
.info-header {
background: linear-gradient(90deg, #5d96b5 0%, #93add5 100%);
padding: 14px 18px;
font-size: 16px;
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
}
.close-btn {
cursor: pointer;
font-size: 20px;
color: #fff;
transition: color 0.2s;
}
.close-btn:hover { color: #ffdddd; }
.info-body {
padding: 18px;
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
}
.status-row {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 14px;
line-height: 1.6;
}
.info-label {
color: #aaa;
flex: 0 0 120px;
}
.info-value {
color: #fff;
flex: 1;
text-align: left;
}`
document.head.appendChild(style)
// 加载地图
setTimeout(() => load_map(), 500)
})
onUnmounted(() => {
// 清除地图相关
if (curMap.value) {
curMap.value.clearMap()
curMap.value.destroy()
curMap.value = null
curAMap.value = null
}
})
</script>
<style>
.border-box-content {
display: flex;
justify-content: center;
align-items: center;
}
/* 移除高德地图相关元素 */
.dg,
.main,
.a {
display: none;
}
.amap-logo,
.amap-copyright {
display: none !important;
}
.amap-ranging-label {
color: black;
}
.project-custom-marker-img {
width: 60px;
height: 44px;
}
</style>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
.custom-cmarker {
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: #fff;
background-size: contain;
}
</style>