Vue 3 Map Component Integrating Multiple Providers with Geocoding

This component provides a flexible way to embed interactive maps within a Vue 3 application, supporting various map providers like Tianditu, Baidu Maps, Tencent Maps, and Amap. It includes geocoding functionality to find coordinates from addresses and supports marker dragging for point selection.

The component can be integrated into your Vue project as follows:

<template>
    <div class="map-container">
        <div id="map-instance" :style="{ width: mapWidth, height: mapHeight }"></div>
    </div>
</template>

<script lang="ts">
import { defineComponent, ref, watch, onMounted } from 'vue';

export default defineComponent({
    props: {
        // The address to initially display or search for
        modelValue: {
            type: String,
            default: '上海市黄浦区上海中心大厦',
        },
        // Whether the marker should be draggable
        draggable: {
            type: Boolean,
            default: true,
        },
        // Width of the map container
        mapWidth: {
            type: String,
            default: '100%',
        },
        // Height of the map container
        mapHeight: {
            type: String,
            default: '350px',
        },
        // Map provider type: 'tianditu', 'baidu', 'tencent', 'amap'
        mapProvider: {
            type: String,
            default: 'baidu',
        },
    },
    emits: ['coordinate-selected'],
    setup(props, { emit }) {
        // --- API Keys (Replace with your actual keys) ---
        const API_KEYS = {
            tianditu: 'YOUR_TIANDITU_KEY',
            baidu: 'YOUR_BAIDU_KEY',
            tencent: 'YOUR_TENCENT_KEY',
            amap: 'YOUR_AMAP_KEY',
            amap_security: 'YOUR_AMAP_SECURITY_KEY',
        };

        const mapInstance = ref<any>(null);
        const longitude = ref(121.47894); // Default longitude (e.g., Shanghai Center)
        const latitude = ref(31.223);    // Default latitude

        // Watch for changes in the address prop to re-geocode
        watch(() => props.modelValue, (newAddress) => {
            if (newAddress && mapInstance.value) {
                geocodeAddress(newAddress);
            }
        });

        onMounted(() => {
            loadMapScript();
        });

        // Dynamically loads the appropriate map script
        const loadMapScript = () => {
            const existingScript = document.querySelector('.map-loader-script');
            if (existingScript) {
                document.body.removeChild(existingScript);
            }

            const script = document.createElement('script');
            script.type = 'text/javascript';
            script.className = 'map-loader-script';

            let scriptUrl = '';
            let callbackFunctionName = 'initializeMap'; // Default callback

            switch (props.mapProvider) {
                case 'tianditu':
                    scriptUrl = `https://api.tianditu.gov.cn/api?v=4.0&tk=${API_KEYS.tianditu}`;
                    break;
                case 'baidu':
                    scriptUrl = `https://api.map.baidu.com/getscript?v=3.0&ak=${API_KEYS.baidu}`;
                    break;
                case 'tencent':
                    // Tencent maps requires a global callback function
                    callbackFunctionName = 'initializeTencentMap';
                    scriptUrl = `https://map.qq.com/api/js?v=2.exp&key=${API_KEYS.tencent}&callback=${callbackFunctionName}`;
                    break;
                case 'amap':
                    scriptUrl = `https://webapi.amap.com/maps?v=2.0&key=${API_KEYS.amap}`;
                    break;
                default:
                    console.error('Unsupported map provider:', props.mapProvider);
                    return;
            }

            script.src = scriptUrl;

            // Handle initialization callback
            if (props.mapProvider === 'tencent') {
                window[callbackFunctionName] = () => {
                    loadTencentGLMapScript(callbackFunctionName);
                };
            } else {
                script.onload = () => {
                    initializeMap();
                };
            }

            document.body.appendChild(script);

            // Special handling for Amap security code
            if (props.mapProvider === 'amap') {
                (window as any)._AMapSecurityConfig = {
                    securityJsCode: API_KEYS.amap_security,
                };
            }
        };

        // Loads the Tencent Maps GL JS SDK if needed
        const loadTencentGLMapScript = (callbackName: string) => {
            const glScript = document.createElement('script');
            glScript.type = 'text/javascript';
            glScript.className = 'map-loader-script-gl'; // Use a different class for GL script
            glScript.src = `https://map.qq.com/api/gljs?v=1.exp&key=${API_KEYS.tencent}&libraries=service`;
            
            const existingGlScript = document.querySelector('.map-loader-script-gl');
             if (existingGlScript) {
                document.body.removeChild(existingGlScript);
            }

            glScript.onload = () => {
                // Re-assign the original callback to ensure it runs after GL script loads
                window[callbackName] = initializeMap;
            };
            document.body.appendChild(glScript);
        };


        // Initializes the map based on the provider
        const initializeMap = () => {
            switch (props.mapProvider) {
                case 'tianditu':
                    initializeTiandituMap();
                    break;
                case 'baidu':
                    initializeBaiduMap();
                    break;
                case 'tencent':
                    initializeTencentMap(); // This is called via callback
                    break;
                case 'amap':
                    initializeAmap();
                    break;
            }
        };

        const initializeTiandituMap = () => {
            const T = (window as any).T;
            if (!T) return;
            const initialPoint = new T.LngLat(longitude.value, latitude.value);
            mapInstance.value = new T.Map('map-instance');
            mapInstance.value.centerAndZoom(initialPoint, 10);
            mapInstance.value.disableScrollWheelZoom();
            mapInstance.value.addControl(new T.Control.Zoom());

            let marker = new T.Marker(initialPoint);
            mapInstance.value.addOverLay(marker);

            if (props.draggable) {
                marker.enableDragging();
                marker.addEventListener('dragend', (event: any) => {
                    updateCoordinatesFromMarker(event.lnglat.lng, event.lnglat.lat, T.LngLat);
                });
            }
        };

        const initializeBaiduMap = () => {
            const BMap = (window as any).BMap;
            if (!BMap) return;
            mapInstance.value = new BMap.Map('map-instance', { enableMapClick: false });
            const initialPoint = new BMap.Point(longitude.value, latitude.value);
            mapInstance.value.centerAndZoom(initialPoint, 10);
            mapInstance.value.addControl(new BMap.NavigationControl({ anchor: window.BMAP_ANCHOR_TOP_LEFT, type: window.BMAP_NAVIGATION_CONTROL_LARGE }));

            let marker = new BMap.Marker(initialPoint);
            mapInstance.value.addOverlay(marker);

            if (props.draggable) {
                marker.enableDragging();
                marker.addEventListener('dragend', (event: any) => {
                    updateCoordinatesFromMarker(event.point.lng, event.point.lat, BMap.Point);
                });
                const copyrightCtrl = new BMap.CopyrightControl({ anchor: window.BMAP_ANCHOR_BOTTOM_RIGHT });
                mapInstance.value.addControl(copyrightCtrl);
                copyrightCtrl.addCopyright({ id: 1, content: '<div class="map-dragging-tips">Drag the marker to set location</div>', bounds: mapInstance.value.getBounds() });
            }
        };

        const initializeTencentMap = () => {
            const qq_maps = (window as any).qq.maps;
            if (!qq_maps) return;
            const initialPoint = new qq_maps.LatLng(latitude.value, longitude.value);
            mapInstance.value = new qq_maps.Map('map-instance', {
                center: initialPoint,
                zoom: 10,
            });

            const marker = new qq_maps.Marker({
                map: mapInstance.value,
                position: initialPoint,
                draggable: props.draggable,
            });

            if (props.draggable) {
                qq_maps.event.addListener(marker, 'dragend', (event: any) => {
                    updateCoordinatesFromMarker(event.latLng.lng, event.latLng.lat, qq_maps.LatLng);
                });
            }
        };

        const initializeAmap = () => {
            const AMap = (window as any).AMap;
            if (!AMap) return;
            mapInstance.value = new AMap.Map('map-instance', {
                zoomEnable: true,
                resizeEnable: false,
                scrollWheel: false,
                zoom: 10,
                center: [longitude.value, latitude.value],
            });
            AMap.plugin(['AMap.ToolBar'], () => {
                mapInstance.value.addControl(new AMap.ToolBar());
            });

            const marker = new AMap.Marker({
                position: mapInstance.value.getCenter(),
                draggable: props.draggable,
            });
            marker.setMap(mapInstance.value);

            if (props.draggable) {
                marker.on('dragend', (event: any) => {
                    updateCoordinatesFromMarker(event.lnglat.lng, event.lnglat.lat, AMap.LngLat);
                });
            }
            mapInstance.value.add(marker);
        };

        // Geocodes an address and updates the map
        const geocodeAddress = (address: string) => {
            switch (props.mapProvider) {
                case 'tianditu':
                    const T = (window as any).T;
                    new T.Geocoder().getPoint(address, (result: any) => {
                        if (result && result.getStatus() === 0) {
                            const location = result.getLocationPoint();
                            updateCoordinates(location.lng, location.lat, T.LngLat);
                        } else {
                            console.warn('Tianditu geocoding failed:', result);
                            // Consider using a fallback or showing a message
                        }
                    });
                    break;
                case 'baidu':
                    const BMap = (window as any).BMap;
                    new BMap.Geocoder().getPoint(address, (point: any) => {
                        if (point) {
                            updateCoordinates(point.lng, point.lat, BMap.Point);
                        } else {
                            console.warn('Baidu geocoding failed for address:', address);
                        }
                    }, '全国');
                    break;
                case 'tencent':
                    // Ensure TMap.service is available after GL script load
                    const TMap = (window as any).TMap;
                     if (TMap && TMap.service) {
                         new TMap.service.Geocoder().getLocation({ address: address }).then((result: any) => {
                             if (result && result.result && result.result.location) {
                                 const coords = result.result.location;
                                 updateCoordinates(coords.lng, coords.lat, TMap.LatLng);
                             } else {
                                 console.warn('Tencent geocoding failed:', result);
                             }
                         });
                    } else {
                        console.error('Tencent Maps service not loaded.');
                    }
                    break;
                case 'amap':
                    const AMap = (window as any).AMap;
                    AMap.plugin('AMap.Geocoder', () => {
                        new AMap.Geocoder().getLocation(address, (status: string, result: any) => {
                            if (status === 'complete' && result.geocodes.length) {
                                const coords = result.geocodes[0].location;
                                updateCoordinates(coords.lng, coords.lat, AMap.LngLat);
                            } else {
                                console.warn('Amap geocoding failed:', result);
                            }
                        });
                    });
                    break;
            }
        };

        // Updates coordinates and emits event
        const updateCoordinates = (lng: number, lat: number, CoordinateClass: any) => {
            longitude.value = lng;
            latitude.value = lat;
            if (mapInstance.value) {
                const newPoint = new CoordinateClass(lng, lat);
                mapInstance.value.setCenter(newPoint);
                // Optionally update marker position if it exists
                // This part might need refinement based on how markers are managed
                 if (props.mapProvider === 'amap') { // Example for AMAP
                    if (mapInstance.value.getMarker) { // Assuming you have a way to get the marker
                       mapInstance.value.getMarker().setPosition([lng, lat]);
                    }
                 } else if (mapInstance.value.getOverlay) { // Example for Baidu/Tianditu
                     const overlays = mapInstance.value.getOverlays ? mapInstance.value.getOverlays() : (mapInstance.value.getComponents ? mapInstance.value.getComponents().markers : []);
                     const marker = overlays.find((o: any) => o.type === 'Marker'); // This logic needs adjustment per provider
                      if(marker) marker.setPosition(newPoint);
                 }
            }
            emit('coordinate-selected', { lng, lat });
        };
        
        // Updates coordinates when marker is dragged
        const updateCoordinatesFromMarker = (lng: number, lat: number, CoordinateClass: any) => {
            longitude.value = lng;
            latitude.value = lat;
            if (mapInstance.value) {
                 mapInstance.value.panTo(new CoordinateClass(lng, lat)); // Pan to the new location
            }
             emit('coordinate-selected', { lng, lat });
        };

        return {
            mapInstance,
            longitude,
            latitude,
            mapWidth: props.mapWidth,
            mapHeight: props.mapHeight,
            loadMapScript,
            initializeMap,
            geocodeAddress,
        };
    },
});
</script>

<style scoped>
.map-container {
    width: 100%;
    height: 350px; /* Default height, can be overridden by props */
    border-radius: 8px; /* Example styling */
}
#map-instance {
    width: 100%;
    height: 100%;
}
.map-dragging-tips {
    padding: 5px;
    background-color: rgba(255, 255, 255, 0.8);
    border-radius: 3px;
}
</style>

Component Usage Example

To use the component, you would include it in another Vue component and bind props and event listeners:

<template>
  <div>
    <MapComponent
      v-model="addressInput"
      :mapProvider="selectedMapType"
      @coordinate-selected="handleCoordinateSelection"
      mapHeight="400px"
    />
    <input v-model="addressInput" placeholder="Enter address"/>
    <select v-model="selectedMapType">
      <option value="tianditu">Tianditu</option>
      <option value="baidu">Baidu Maps</option>
      <option value="tencent">Tencent Maps</option>
      <option value="amap">Amap</option>
    </select>
  </div>
</template>

<script lang="ts">
import { ref } from 'vue';
import MapComponent from './MapComponent.vue'; // Adjust path as needed

export default {
  components: {
    MapComponent,
  },
  setup() {
    const addressInput = ref('上海市黄浦区上海中心大厦');
    const selectedMapType = ref('baidu'); // Default map type

    // Callback function to receive selected coordinates
    const handleCoordinateSelection = (coords: { lng: number; lat: number }) => {
      console.log('Selected Coordinates:', coords);
      // Update your form or state with coords.lng and coords.lat
    };

    return {
      addressInput,
      selectedMapType,
      handleCoordinateSelection,
    };
  },
};
</script>

Props

  • v-model (String): Binds to the address input. Changes to this value will trigger a geocoding search.
  • mapProvider (String, default: 'baidu'): Specifies which map service to use ('tianditu', 'baidu', 'tencent', 'amap').
  • draggable (Booleen, default: true): Determines if the map marker can be dragged.
  • mapHeight (String, default: '350px'): Sets the height of the map container.
  • mapWidth (String, default: '100%'): Sets the width of the map container.

Events

  • coordinate-selected: Emitted when the map view is updated (either by goecoding or marker drag). It passes an object containing the lng (longitude) and lat (latitude) of the selected point.

Tags: vue3 maps geolocation geocoding tianditu

Posted on Sat, 20 Jun 2026 17:52:32 +0000 by wtg21.org