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);
- 逆地理编码: