import axios, { AxiosInstance, AxiosResponse } from 'axios';// 高德地圖 API 響應基礎結構
interface AMapResponse {status: string;info: string;infocode: string;
}// 逆地理編碼響應結構
interface RegeoResponse extends AMapResponse {regeocode: {formatted_address: string;addressComponent: {province: string;city: string;district: string;township: string;citycode: string;adcode: string;};pois?: Array<{ id: string; name: string; address: string }>;roads?: Array<{ id: string; name: string }>;roadinters?: Array<{ direction: string; distance: string }>;};
}// 地理編碼響應結構
interface GeoResponse extends AMapResponse {geocodes: Array<{location: string;formatted_address: string;level: string;city: string;district: string;adcode: string;}>;
}// POI 搜索響應結構
interface POISearchResponse extends AMapResponse {pois: Array<{id: string;name: string;type: string;typecode: string;address: string;location: string;pname: string;cityname: string;adname: string;}>;
}// 輸入提示響應結構
interface InputTipsResponse extends AMapResponse {tips: Array<{id: string;name: string;district: string;location: string;}>;
}// 距離計算響應結構
interface DistanceResponse extends AMapResponse {results: Array<{distance: string;duration: string;}>;
}// 工具類配置接口
interface AMapConfig {apiKey: string;baseUrl?: string;
}// 逆地理編碼選項
interface RegeoOptions {radius?: number;extensions?: 'base' | 'all';poiType?: string;roadLevel?: number;batch?: boolean;
}// POI 搜索選項
interface POISearchOptions {page?: number;offset?: number;extensions?: 'base' | 'all';types?: string;
}class AMapService {private axiosInstance: AxiosInstance;private apiKey: string;private baseUrl: string;/*** 構造函數* @param config - 配置對象,包含 API Key 和可選的基礎 URL*/constructor(config: AMapConfig) {this.apiKey = config.apiKey;this.baseUrl = config.baseUrl || 'https://restapi.amap.com/v3';// 初始化 Axios 實例this.axiosInstance = axios.create({baseURL: this.baseUrl,timeout: 10000,headers: {'Content-Type': 'application/json',},});}/*** 發起通用請求* @param endpoint - API 端點* @param params - 請求參數* @returns 請求結果* @private*/private async _request<T>(endpoint: string, params: Record<string, any> = {}): Promise<T> {const baseParams = {key: this.apiKey,output: 'JSON',};// 合并參數并移除空值const queryParams = { ...baseParams, ...params };Object.keys(queryParams).forEach((key) => {if (queryParams[key] === undefined || queryParams[key] === null) {delete queryParams[key];}});try {const response: AxiosResponse<T> = await this.axiosInstance.get(endpoint, { params: queryParams });const data = response.data;if (data.status !== '1') {throw new Error(`高德API錯誤: ${data.info} (錯誤碼: ${data.infocode})`);}return data;} catch (error) {console.error(`高德地圖請求失敗 (${endpoint}):`, error);throw error;}}/*** 逆地理編碼 - 根據經緯度獲取地址信息* @param longitude - 經度* @param latitude - 緯度* @param options - 額外選項* @returns 地址信息*/async regeoCode(longitude: number, latitude: number, options: RegeoOptions = {}): Promise<{province: string;city: string;district: string;township: string;citycode: string;adcode: string;formattedAddress: string;pois: Array<{ id: string; name: string; address: string }>;roads: Array<{ id: string; name: string }>;roadinters: Array<{ direction: string; distance: string }>;rawData: RegeoResponse;}> {const params = {location: `${longitude},${latitude}`,radius: options.radius || 1000,extensions: options.extensions || 'base',poitype: options.poiType,roadlevel: options.roadLevel || 0,batch: options.batch || false,};const data = await this._request<RegeoResponse>('geocode/regeo', params);if (data.regeocode) {const addressComponent = data.regeocode.addressComponent;return {province: addressComponent.province,city: addressComponent.city || addressComponent.province, // 處理直轄市district: addressComponent.district,township: addressComponent.township,citycode: addressComponent.citycode,adcode: addressComponent.adcode,formattedAddress: data.regeocode.formatted_address,pois: data.regeocode.pois || [],roads: data.regeocode.roads || [],roadinters: data.regeocode.roadinters || [],rawData: data,};}throw new Error('未找到地址信息');}/*** 地理編碼 - 根據地址描述獲取經緯度* @param address - 地址描述* @param city - 城市限定(可選)* @returns 經緯度信息*/async geoCode(address: string, city: string | null = null): Promise<{longitude: number;latitude: number;formattedAddress: string;level: string;city: string;district: string;adcode: string;rawData: GeoResponse['geocodes'][0];}> {const params = { address, city };const data = await this._request<GeoResponse>('geocode/geo', params);if (data.geocodes && data.geocodes.length > 0) {const geocode = data.geocodes[0];const [longitude, latitude] = geocode.location.split(',').map(Number);return {longitude,latitude,formattedAddress: geocode.formatted_address,level: geocode.level,city: geocode.city,district: geocode.district,adcode: geocode.adcode,rawData: geocode,};}throw new Error('未找到對應的地理位置');}/*** 關鍵字搜索 POI(興趣點)* @param keywords - 關鍵字* @param city - 城市限定* @param options - 額外選項* @returns POI 列表*/async searchPOI(keywords: string, city: string | null = null, options: POISearchOptions = {}): Promise<Array<{id: string;name: string;type: string;typecode: string;address: string;location: { longitude: number; latitude: number };pname: string;cityname: string;adname: string;rawData: POISearchResponse['pois'][0];}>> {const params = {keywords,city: city || '全國',page: options.page || 1,offset: options.offset || 20,extensions: options.extensions || 'base',types: options.types,};const data = await this._request<POISearchResponse>('place/text', params);if (data.pois && data.pois.length > 0) {return data.pois.map((poi) => ({id: poi.id,name: poi.name,type: poi.type,typecode: poi.typecode,address: poi.address,location: {longitude: parseFloat(poi.location.split(',')[0]),latitude: parseFloat(poi.location.split(',')[1]),},pname: poi.pname,cityname: poi.cityname,adname: poi.adname,rawData: poi,}));}return [];}/*** 輸入提示(自動完成)* @param keywords - 關鍵詞* @param city - 城市限定* @returns 提示列表*/async inputTips(keywords: string, city: string | null = null): Promise<Array<{id: string;name: string;district: string;location: string;}>> {const params = {keywords,city: city || '全國',type: 'all',};const data = await this._request<InputTipsResponse>('assistant/inputtips', params);return data.tips || [];}/*** 批量逆地理編碼(最多 20 個點)* @param locations - 經緯度數組 [{longitude, latitude}, ...]* @returns 批量結果*/async batchRegeoCode(locations: Array<{ longitude: number; latitude: number }>): Promise<RegeoResponse['regeocode'][]> {if (!Array.isArray(locations) || locations.length === 0) {throw new Error('位置數組不能為空');}if (locations.length > 20) {throw new Error('批量查詢最多支持 20 個點');}const locationStr = locations.map((loc) => `${loc.longitude},${loc.latitude}`).join('|');const data = await this._request<RegeoResponse>('geocode/regeo', {location: locationStr,batch: true,});return data.regeocodes || [];}/*** 計算兩點間距離* @param lng1 - 起點經度* @param lat1 - 起點緯度* @param lng2 - 終點經度* @param lat2 - 終點緯度* @returns 距離信息(米)*/async calculateDistance(lng1: number, lat1: number, lng2: number, lat2: number): Promise<{distance: number;duration: number;}> {const data = await this._request<DistanceResponse>('distance', {origins: `${lng1},${lat1}`,destination: `${lng2},${lat2}`,type: 1, // 直線距離});if (data.results && data.results.length > 0) {return {distance: parseInt(data.results[0].distance),duration: parseInt(data.results[0].duration),};}throw new Error('距離計算失敗');}
}// 單例模式
let amapInstance: AMapService | null = null;/*** 初始化高德地圖服務* @param apiKey - API Key* @returns 服務實例*/
export function initAMapService(apiKey: string): AMapService {if (!amapInstance) {amapInstance = new AMapService({ apiKey });}return amapInstance;
}/*** 獲取高德地圖服務實例* @returns 服務實例*/
export function getAMapService(): AMapService {if (!amapInstance) {throw new Error('請先調用 initAMapService 初始化');}return amapInstance;
}export default AMapService;
使用說明
- 申請 API Key:在高德地圖開放平臺(https://lbs.amap.com/)注冊并申請 Web 服務 API Key。
- 安裝依賴:
- 確保項目中已安裝 axios 和 TypeScript:npm install axios typescript.
- 在 tsconfig.json 中啟用 esModuleInterop 和 strict 選項以確保類型安全。
- 初始化服務:
typescript
import { initAMapService } from './AMapService';const amapService = initAMapService('您的API_KEY');
- 功能調用示例:
- 逆地理編碼:
typescript
const addressInfo = await amapService.regeoCode(116.480881, 39.989410, { extensions: 'all' }); console.log(addressInfo.formattedAddress, addressInfo.addressComponent);
- 地理編碼:
typescript
const geoInfo = await amapService.geoCode('北京市朝陽區望京街', '北京'); console.log(geoInfo.longitude, geoInfo.latitude);
- POI 搜索:
typescript
const pois = await amapService.searchPOI('咖啡', '北京', { page: 1, offset: 10 }); console.log(pois);
- 輸入提示:
typescript
const tips = await amapService.inputTips('故宮', '北京'); console.log(tips);
- 批量逆地理編碼:
typescript
const locations = [{ longitude: 116.480881, latitude: 39.989410 },{ longitude: 116.481026, latitude: 39.989614 }, ]; const batchResult = await amapService.batchRegeoCode(locations); console.log(batchResult);
- 距離計算:
typescript
const distanceInfo = await amapService.calculateDistance(116.480881, 39.989410, 116.481026, 39.989614); console.log(distanceInfo.distance, distanceInfo.duration);
- 逆地理編碼: