123 lines
4.2 KiB
TypeScript
123 lines
4.2 KiB
TypeScript
|
|
// 高德 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<string, unknown>) => AMapGeolocation
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
declare global {
|
|||
|
|
interface Window {
|
|||
|
|
AMap?: AMapNamespace
|
|||
|
|
_AMapSecurityConfig?: { securityJsCode: string }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let sdkPromise: Promise<AMapNamespace> | null = null
|
|||
|
|
|
|||
|
|
/** 按需注入高德 JSAPI 脚本(单例)。安全密钥须在脚本加载前挂到 window。 */
|
|||
|
|
function loadSdk(): Promise<AMapNamespace> {
|
|||
|
|
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<GeoResult | null> {
|
|||
|
|
if (!KEY) return null
|
|||
|
|
|
|||
|
|
let AMap: AMapNamespace
|
|||
|
|
try {
|
|||
|
|
AMap = await loadSdk()
|
|||
|
|
} catch {
|
|||
|
|
return null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return new Promise<GeoResult | null>((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)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
}
|