// 高德 JS API(AMap.Geolocation)前端定位封装。 // // 设计: // - Key 与安全密钥都从环境变量读,**不写死**:`VITE_AMAP_JS_KEY` / `VITE_AMAP_JS_CODE`。 // 两者是高德「Web端(JS API)」专属密钥,和后端的 Web 服务 REST `AMAP_KEY` 是两套。 // - 未配置 Key → `locate()` 直接返回 null,由后端 IP 定位兜底(即旧行为,绝不退化)。 // - 定位两段式:先浏览器原生定位(GPS/WiFi,精度高,但**需 HTTPS**,非 localhost 的 // HTTP 页面下浏览器会禁用);失败/被拒 → 退回高德纯 IP 城市定位(getCityInfo)。 // - 任何失败都吞掉返回 null(不打扰页面),后端继续兜底。 const KEY = import.meta.env.VITE_AMAP_JS_KEY as string | undefined const CODE = import.meta.env.VITE_AMAP_JS_CODE as string | undefined export interface GeoResult { /** 经度 */ lon: number /** 纬度 */ lat: number /** 城市名(直辖市/定位精度不足时可能为空) */ city?: string } // 高德 SDK 没有官方类型,这里只声明用到的最小子集。 interface AMapGeolocation { getCurrentPosition(cb: (status: string, result: GeoPositionResult) => void): void getCityInfo(cb: (status: string, result: GeoCityResult) => void): void } interface GeoPositionResult { position?: { lng: number; lat: number } addressComponent?: { city?: string; province?: string } } interface GeoCityResult { /** [lng, lat] 城市中心 */ center?: [number, number] city?: string province?: string } interface AMapNamespace { plugin(name: string, onload: () => void): void Geolocation: new (opts: Record) => AMapGeolocation } declare global { interface Window { AMap?: AMapNamespace _AMapSecurityConfig?: { securityJsCode: string } } } let sdkPromise: Promise | null = null /** 按需注入高德 JSAPI 脚本(单例)。安全密钥须在脚本加载前挂到 window。 */ function loadSdk(): Promise { if (sdkPromise) return sdkPromise sdkPromise = new Promise((resolve, reject) => { if (!KEY) return reject(new Error('AMap JS key not configured')) if (window.AMap) return resolve(window.AMap) if (CODE) window._AMapSecurityConfig = { securityJsCode: CODE } const s = document.createElement('script') s.src = `https://webapi.amap.com/maps?v=2.0&key=${encodeURIComponent(KEY)}&plugin=AMap.Geolocation` s.async = true s.onload = () => window.AMap ? resolve(window.AMap) : reject(new Error('AMap SDK init failed')) s.onerror = () => reject(new Error('AMap SDK script load error')) document.head.appendChild(s) }) return sdkPromise } /** * 取当前位置。未配置 Key 或定位失败时返回 null(交给后端兜底)。 * 不会抛错。 */ export async function locate(): Promise { if (!KEY) return null let AMap: AMapNamespace try { AMap = await loadSdk() } catch { return null } return new Promise((resolve) => { let settled = false const finish = (r: GeoResult | null) => { if (!settled) { settled = true resolve(r) } } // 整体兜底超时,避免插件卡死不回调。 const timer = setTimeout(() => finish(null), 12_000) const done = (r: GeoResult | null) => { clearTimeout(timer) finish(r) } AMap.plugin('AMap.Geolocation', () => { const geo = new AMap.Geolocation({ enableHighAccuracy: true, timeout: 10_000 }) geo.getCurrentPosition((status, result) => { if (status === 'complete' && result.position) { done({ lon: result.position.lng, lat: result.position.lat, city: result.addressComponent?.city || result.addressComponent?.province, }) } else { // 原生/精确定位失败(多因 HTTP 下被禁或用户拒绝)→ 退回纯 IP 城市定位。 geo.getCityInfo((s2, r2) => { if (s2 === 'complete' && r2.center) { done({ lon: r2.center[0], lat: r2.center[1], city: r2.city || r2.province }) } else { done(null) } }) } }) }) }) }