代码:
<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)"/>
`
function flyTo (targetZoom, center, duration = 800) {
const map = curMap.value
const startZoom = map.getZoom()
const startCenter = map.getCenter()
const start = Date.now()
const easeOutCubic = t => 1 - Math.pow(1 - t, 3)
function frame () {
const t = Math.min((Date.now() - start) / duration, 1)
const eased = easeOutCubic(t)
const zoom = startZoom + (targetZoom - startZoom) * eased
const lng = startCenter.lng + (center[0] - startCenter.lng) * eased
const lat = startCenter.lat + (center[1] - startCenter.lat) * eased
map.setZoomAndCenter(zoom, [lng, lat], false) // 关闭官方过渡
if (t < 1) requestAnimationFrame(frame)
}
requestAnimationFrame(frame)
}
//地图及边界
const load_map = async () => {
map_loading.value = true
curMap.value = null
let query = {
pageSize:10000,
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 ''
}
//点聚合逻辑
const handlerMarkerCluster = (points) => {
curAMap.value = new AMap.MarkerCluster(
curMap.value,
points,
{
gridSize: 90,
renderClusterMarker: handlerRenderClusterMarker, // 自定义聚合点样式
renderMarker: handlerRenderMarker // 自定义点样式
}
);
// 聚合点点击事件
curAMap.value.on('click', (e) => {
const { clusterData } = e
if (!clusterData || clusterData.length <= 1) return
const center = clusterData.reduce(
(acc, cur) => ({ lng: acc.lng + cur.lnglat.lng, lat: acc.lat + cur.lnglat.lat }),
{ lng: 0, lat: 0 }
)
center.lng /= clusterData.length
center.lat /= clusterData.length
flyTo(curMap.value.getZoom() + 3, [center.lng, center.lat], 800)
})
}
// 自定义聚合标记
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)
context.marker.setOffset(new AMap.Pixel(0, -24))
}
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_red.png`, import.meta.url).href;
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 handlerRenderMarker = (context) => {
const { projectPeriod } = context.data[0].info
// 图标
const iconSrc = statusIcon(projectPeriod)
context.marker.setContent(
`<img class="project-custom-marker-img" src="${iconSrc}" />`)
context.marker.setOffset(new AMap.Pixel(0, -44)) // x 不变,y 负一半高度
// 关键:点击打开与原来样式一致的 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: -34 }
})
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>