PersonalWebApplication/Client/src/lib/amap.ts
cc 4c8c4360ad feat(weather): 起始页天气/定位细节完善
- 新增 Client/src/lib/amap.ts:高德 JS 定位封装
- weather.ts / useWeather.ts:带经纬度/城市请求,定位失败不兜底
- Server weather.rs:相应调整
- 文档同步;.gitignore 忽略本地 .codegraph 索引

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:45:39 +08:00

123 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 高德 JS APIAMap.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)
}
})
}
})
})
})
}