A TMap Utility for AMap in Vue 3: Markers, Areas, Loca Pulse Lines, and Vue-Powered InfoWindows
Vue 3 + AMap Wrapper in Practice: Points, Regions, Loca Pulse Lines, and Vue InfoWindows
Tech stack: Vue 3 + TypeScript + AMap JS API 2.0 + Loca 2.0
This article provides a unified wrapper around AMap's capabilities, and will later cover common map-backend page features and examples using the wrapped API, along with the underlying implementation details of the wrapper.
1. Wrapper Capability Overview
The TMap wrapper in the project provides the following methods:
TMap
├── initMap()
├── setMapCenter()
├── setMapStyle()
├── addMarker()
├── addMarkers()
├── setMarkerIcon()
├── markerDraggable()
├── addPolygons()
├── drawPolygonTool()
├── updatePolygon()
├── addPolylines()
├── setPolylineDisplay()
├── addPulseLayer()
├── removePulseLayer()
├── addCluster()
├── setClusterData()
├── createdInfoWindow()
├── openInfoWindow()
└── closeInfoWindow()
Usage on a page:
import TMap from '@/utils/tmap'
const tmap = new TMap()
onMounted(() => {
tmap.initMap('map-container', () => {
initMapData()
})
})
2. Map Initialization
const tmap = new TMap()
tmap.initMap('map-container', () => {
tmap.setMapStyle('amap://styles/your-style-id')
tmap.setMapCenter([116.397428, 39.90923], 14)
})
Under the hood it uses @amap/amap-jsapi-loader to load AMap 2.0:
AMapLoader.load({
key,
version: '2.0',
plugins: [
'AMap.MouseTool',
'AMap.Geocoder',
'AMap.PolygonEditor',
'AMap.DistrictSearch',
'AMap.PolylineEditor',
'AMap.MarkerCluster'
],
Loca: {
version: '2.0.0'
}
})
3. Point Rendering
1. Adding Points Using the Wrapper
const pointList = [
{
id: 1,
name: 'Point One',
shape: [116.397428, 39.90923],
icon: 'point-icon',
color: '#2484f4'
},
{
id: 2,
name: 'Point Two',
shape: [116.407428, 39.91923],
icon: 'point-icon',
color: '#2484f4'
}
]
tmap.addMarkers(
pointList,
(item) => {
console.log('Clicked point', item)
},
true
)
Internally, addMarkers() converts the config into AMap.Marker:
const marker = new AMap.Marker({
position: item.shape,
offset: new AMap.Pixel(-14, -32),
content: this.dealMarkerIcon(item.icon, item.color, []),
extData: item
})
marker.on('click', () => {
onClick?.(item)
})
map.add(marker)
2. Changing Point Icon and Position
tmap.setMarkerPosition([116.4, 39.91])
tmap.setMarkerIcon('point-active', '#00ffff', [36, 44])
3. Point Dragging
tmap.markerSetDraggable(true)
tmap.markerDraggable((event) => {
const lnglat = [event.lnglat.lng, event.lnglat.lat]
tmap.regeoCode(lnglat, (addressInfo) => {
console.log('Address after drag', addressInfo.address)
})
})
4. Region Drawing and Rendering
1. Rendering Regions
const areaList = [
{
id: 1,
name: 'Example Region',
shape: [
[116.39, 39.9],
[116.41, 39.9],
[116.41, 39.92],
[116.39, 39.92]
],
color: '#2484f4'
}
]
tmap.addPolygons(
areaList,
(area) => {
console.log('Clicked region', area)
},
{
show: true
}
)
Under the hood it creates:
const polygon = new AMap.Polygon({
path: item.shape,
fillColor: item.color,
fillOpacity: 0.3,
strokeColor: item.color,
strokeWeight: 4,
extData: item,
zIndex: 50
})
2. Drawing Regions
tmap.drawPolygonTool('#2484f4', (path) => {
console.log('Drawing complete', path)
})
3. Editing Regions
tmap.updatePolygon((path) => {
const normalizedPath = path.map((point) => [
Number(point.lng),
Number(point.lat)
])
console.log('Edited path', normalizedPath)
})
5. Administrative District Mask
tmap.maskCoverMapOtherArea({
searchVal: 'Example District',
center: [116.397428, 39.90923],
zoom: 14,
fillColor: '#040b31',
fillOpacity: 0.8,
strokeColor: '#2484f4'
})
Under the hood it uses AMap.DistrictSearch to get the administrative district boundaries, then covers the outside area with a polygon that has a hole.
const district = new AMap.DistrictSearch({
subdistrict: 1,
extensions: 'all',
level: 'district'
})
district.search(keyword, (status, result) => {
if (status !== 'complete') return
const holes = result.districtList[0].boundaries
const outer = [
new AMap.LngLat(-360, 90, true),
new AMap.LngLat(-360, -90, true),
new AMap.LngLat(360, -90, true),
new AMap.LngLat(360, 90, true)
]
map.add(new AMap.Polygon({
path: [outer, ...holes],
fillColor: '#040b31',
fillOpacity: 0.8,
strokeColor: '#2484f4',
strokeWeight: 4
}))
})
6. Routes and Loca Pulse Lines
1. Business Route Data Structure
interface FlowLine {
id: string | number
name: string
shape: Array<[number, number]>
color?: string
width?: number
}
2. Adding Base Polylines
const lineList: FlowLine[] = [
{
id: 1,
name: 'Example Flow',
shape: [
[116.397428, 39.90923],
[116.405428, 39.91523],
[116.412428, 39.91923]
],
color: '#00d8ff',
width: 4
}
]
tmap.addPolylines(lineList, (line) => {
console.log('Clicked line', line)
})
3. Converting Routes to GeoJSON
Loca's PulseLineLayer requires standard GeoJSON:
function formatGeoData(list: FlowLine[]) {
return list.reduce((features: any[], line) => {
if (line.shape?.length >= 2) {
features.push({
type: 'Feature',
properties: {
id: line.id,
name: line.name,
color: line.color || '#00d8ff',
width: line.width || 4
},
geometry: {
type: 'LineString',
coordinates: line.shape
}
})
}
return features
}, [])
}
const geo = {
type: 'FeatureCollection',
features: formatGeoData(lineList)
}
4. Adding a Pulse Layer Using the Wrapper
tmap.addPulseLayer(geo)
The project's addPulseLayer() creates a Loca pulse layer:
addPulseLayer(geo: GeoJSON.FeatureCollection) {
if (!this.pulseLayer) {
this.pulseLayer = new this.Loca.PulseLineLayer({
zIndex: 1000,
opacity: 1,
visible: true,
zooms: [2, 22]
})
}
this.pulseLayer.setStyle({
altitude: 0,
lineWidth: (_, feature) => Number(feature.properties.width),
headColor: (_, feature) => feature.properties.color,
trailColor: 'rgba(128,128,128,.5)',
interval: 0.4,
duration: 4000
})
this.pulseLayer.setSource(
new this.Loca.GeoJSONSource({ data: geo })
)
this.loca.add(this.pulseLayer)
this.loca.animate.start()
}
5. Adding a New Pulse Line Route
When business data adds a new route, merge it into the route list and regenerate the GeoJSON:
const currentLines: FlowLine[] = []
function addFlowLine(line: FlowLine) {
currentLines.push(line)
// This wrapper replaces the whole layer; remove the old pulse layer first
tmap.removePulseLayer()
tmap.addPulseLayer({
type: 'FeatureCollection',
features: formatGeoData(currentLines)
})
}
Example:
addFlowLine({
id: 2,
name: 'New Flow',
shape: [
[116.397428, 39.90923],
[116.417428, 39.92923]
],
color: '#00e5ff',
width: 5
})
6. Show, Hide, and Remove
// Controls both normal Polylines and the Loca pulse layer
tmap.setPolylineDisplay('show')
tmap.setPolylineDisplay('hide')
// Remove all pulse layers and stop the animation
tmap.removePulseLayer()
7. Point Clustering
const markers = pointList.map((point) => {
return new AMap.Marker({
position: point.shape,
extData: point
})
})
tmap.addCluster(markers)
Updating cluster data:
tmap.setClusterData(newMarkers, 'reset')
Appending data:
tmap.setClusterData(newMarkers, 'add')
8. InfoWindow Mounting Custom Vue Components
1. Rendering Vue Components in the Parent Template
<div ref="infoWindowRef" class="map-info-window">
<GeneralDetail
:type="state.displayType"
:info="state.displayData"
@on-close="handleCloseInfoWindow"
@on-check="state.showFullDetail = true"
/>
</div>
The key points here are:
GeneralDetailis created and mounted by Vue normally;infoWindowRef.valueis a real DOM container;TMaponly hands this DOM container to AMap's InfoWindow.
2. Map Service Creating an InfoWindow
createdInfoWindow(content: HTMLElement, closeWhenClickMap = true) {
this.infoWindow = new this.AMap.InfoWindow({
isCustom: true,
content,
closeWhenClickMap,
offset: new this.AMap.Pixel(0, -50)
})
}
openInfoWindow(position: [number, number]) {
this.infoWindow?.open(this.map, position)
}
3. Clicking a Point to Update and Open
async function openPointInfoWindow(point) {
state.displayType = 'point'
state.displayData = point
// Wait for Vue to render the latest data into the container
await nextTick()
if (!tmap.infoWindow) {
tmap.createdInfoWindow(infoWindowRef.value, false)
}
tmap.openInfoWindow(point.shape)
}
Since GeneralDetail is always managed by Vue, after updating state.displayData, the component updates reactively without needing to recreate the Vue instance.
4. Closing and Destroying
function handleCloseInfoWindow() {
tmap.closeInfoWindow()
}
onBeforeUnmount(() => {
tmap.closeInfoWindow()
tmap.destroyInfoWindow()
})
5. Dynamically Creating a Vue Component as InfoWindow
If the InfoWindow content is not pre-mounted in the template but created at runtime, handle it like this:
import { createApp, h, reactive, nextTick } from 'vue'
import InfoPopup from './InfoPopup.vue'
let popupApp: ReturnType<typeof createApp> | null = null
let popupHost: HTMLElement | null = null
let popupWindow: AMap.InfoWindow | null = null
async function openDynamicInfoWindow(data: any, position: [number, number]) {
closeDynamicInfoWindow()
popupHost = document.createElement('div')
const props = reactive({ data })
popupApp = createApp({
render() {
return h(InfoPopup, {
data: props.data,
onClose: closeDynamicInfoWindow
})
}
})
popupApp.mount(popupHost)
await nextTick()
popupWindow = new AMap.InfoWindow({
isCustom: true,
content: popupHost,
offset: new AMap.Pixel(0, -20)
})
popupWindow.open(map, position)
}
function closeDynamicInfoWindow() {
popupWindow?.close()
popupWindow = null
popupApp?.unmount()
popupApp = null
popupHost = null
}
When mounting dynamically, note:
createAppcreates a new Vue application instance;- If the popup component uses Pinia, Router, or i18n, you need to install those plugins on the new app, or pass shared state as props;
- After closing the InfoWindow, you must call
unmount(), otherwise you may end up with duplicate instances and memory leaks; - Pre-rendering the DOM in the parent component is preferred because it reuses the current Vue application context.
9. Complete Usage Example
import { nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import TMap from '@/utils/tmap'
import GeneralDetail from './GeneralDetail.vue'
const mapRef = ref()
const infoWindowRef = ref<HTMLElement>()
const tmap = new TMap()
const state = reactive({
displayType: 'point',
displayData: {}
})
onMounted(() => {
tmap.initMap('map-container', async () => {
tmap.setMapStyle('amap://styles/your-style-id')
const points = await getPointList()
const areas = await getAreaList()
const lines = await getLineList()
tmap.addMarkers(points, handlePointClick, true)
tmap.addPolygons(areas, handleAreaClick)
tmap.addPolylines(lines)
tmap.addPulseLayer({
type: 'FeatureCollection',
features: formatGeoData(lines)
})
})
})
async function handlePointClick(point: any) {
state.displayType = 'point'
state.displayData = point
await nextTick()
if (!tmap.infoWindow) {
tmap.createdInfoWindow(infoWindowRef.value!, false)
}
tmap.openInfoWindow(point.shape)
}
async function handleAreaClick(area: any) {
state.displayType = 'area'
state.displayData = area
await nextTick()
if (!tmap.infoWindow) {
tmap.createdInfoWindow(infoWindowRef.value!, false)
}
tmap.openInfoWindow(getPolygonCenter(area.shape))
}
function getPolygonCenter(shape: Array<[number, number]>) {
const [lng, lat] = shape.reduce(
(total, point) => [total[0] + point[0], total[1] + point[1]],
[0, 0]
)
return [lng / shape.length, lat / shape.length] as [number, number]
}
onBeforeUnmount(() => {
tmap.removePulseLayer()
tmap.closeInfoWindow()
tmap.destroyInfoWindow()
})
10. Feature-to-Wrapper-Method Mapping
| Feature | Wrapper Method |
|---|---|
| Map initialization | initMap() |
| Map center and zoom | setMapCenter() |
| Map style | setMapStyle() |
| Single Marker | addMarker() |
| Batch Markers | addMarkers() |
| Marker dragging | markerDraggable() |
| Reverse geocoding | regeoCode() |
| Region rendering | addPolygons() |
| Region drawing | drawPolygonTool() |
| Region editing | updatePolygon() |
| Route rendering | addPolylines() |
| Route and pulse line visibility | setPolylineDisplay() |
| Add pulse line | addPulseLayer() |
| Remove pulse lines | removePulseLayer() |
| Point clustering | addCluster() |
| Update clustering | setClusterData() |
| Create info window | createdInfoWindow() |
| Open info window | openInfoWindow() |
| Close info window | closeInfoWindow() |
Summary
The AMap wrapper handles four kinds of capabilities:
- Map instance and base map;
- Marker, Polygon, Polyline;
- Loca pulse lines and clustering;
- InfoWindow cooperation with Vue components.
For InfoWindow using custom Vue components, the simplest and most stable approach is to first render a container in the parent template, then pass that container's DOM to AMap.InfoWindow. This way Vue's reactivity, Pinia, Router, and component styles are all reused normally.
Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.
Is it open source?
No, just sharing examples of functionality I've built.