145 lines
3.8 KiB
TypeScript
145 lines
3.8 KiB
TypeScript
import { getWechatLocationDetailed } from '@dukang/weixin-sdk';
|
|
import { apiBase } from './api';
|
|
import { weixinSdk } from './weixin';
|
|
import { regionFromGeo, type RegionSelection } from './region-data';
|
|
|
|
export const GPS_CITY_STORAGE_KEY = 'dukang_gps_city';
|
|
export const CITY_STORAGE_KEY = 'dukang_selected_city';
|
|
export const FALLBACK_CITY_CODE = '410100';
|
|
|
|
export type ResolvedUserCity = {
|
|
province: string;
|
|
city: string;
|
|
district: string;
|
|
cityCode?: string;
|
|
cityName?: string;
|
|
openCity: boolean;
|
|
region: RegionSelection;
|
|
displayCity: string;
|
|
};
|
|
|
|
type GpsCityCache = ResolvedUserCity & { timestamp: number };
|
|
|
|
function readCache(): GpsCityCache | null {
|
|
try {
|
|
const raw = sessionStorage.getItem(GPS_CITY_STORAGE_KEY);
|
|
if (!raw) return null;
|
|
const parsed = JSON.parse(raw) as GpsCityCache;
|
|
if (Date.now() - parsed.timestamp > 30 * 60 * 1000) return null;
|
|
return parsed;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function writeCache(data: ResolvedUserCity) {
|
|
sessionStorage.setItem(
|
|
GPS_CITY_STORAGE_KEY,
|
|
JSON.stringify({ ...data, timestamp: Date.now() } satisfies GpsCityCache),
|
|
);
|
|
}
|
|
|
|
async function reportLocationToServer(payload: {
|
|
latitude?: number;
|
|
longitude?: number;
|
|
sdk: 'jssdk' | 'geolocation';
|
|
status: 'success' | 'fail';
|
|
errMsg?: string;
|
|
}) {
|
|
const token = localStorage.getItem('accessToken');
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
'X-Client-App': 'USER_H5',
|
|
};
|
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
|
|
const res = await fetch(`${apiBase}/common/wechat/location`, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const json = await res.json();
|
|
if (json.code !== 0) {
|
|
throw new Error(json.message || '定位上报失败');
|
|
}
|
|
return json.data as {
|
|
province?: string;
|
|
city?: string;
|
|
district?: string;
|
|
cityCode?: string;
|
|
cityName?: string;
|
|
openCity?: boolean;
|
|
};
|
|
}
|
|
|
|
function toResolved(data: {
|
|
province?: string;
|
|
city?: string;
|
|
district?: string;
|
|
cityCode?: string;
|
|
cityName?: string;
|
|
openCity?: boolean;
|
|
}): ResolvedUserCity | null {
|
|
if (!data.province || !data.city) return null;
|
|
const region = regionFromGeo(data.province, data.city, data.district);
|
|
const displayCity = data.cityName ?? (data.city.endsWith('市') ? data.city : `${data.city}市`);
|
|
return {
|
|
province: data.province,
|
|
city: data.city,
|
|
district: data.district ?? '',
|
|
cityCode: data.cityCode,
|
|
cityName: data.cityName,
|
|
openCity: !!data.openCity,
|
|
region,
|
|
displayCity,
|
|
};
|
|
}
|
|
|
|
/** 获取并解析用户当前城市(微信 JSSDK 优先),失败返回 null */
|
|
export async function resolveUserCity(force = false): Promise<ResolvedUserCity | null> {
|
|
if (!force) {
|
|
const cached = readCache();
|
|
if (cached) return cached;
|
|
}
|
|
|
|
const outcome = await getWechatLocationDetailed({
|
|
apiBase,
|
|
clientApp: 'USER_H5',
|
|
getAccessToken: () => localStorage.getItem('accessToken'),
|
|
});
|
|
|
|
if (!outcome.location) {
|
|
await reportLocationToServer({
|
|
sdk: outcome.sdk,
|
|
status: 'fail',
|
|
errMsg: outcome.errMsg,
|
|
}).catch(() => {});
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const data = await reportLocationToServer({
|
|
latitude: outcome.location.latitude,
|
|
longitude: outcome.location.longitude,
|
|
sdk: outcome.sdk,
|
|
status: 'success',
|
|
});
|
|
const resolved = toResolved(data);
|
|
if (resolved) {
|
|
writeCache(resolved);
|
|
if (resolved.openCity && resolved.cityCode) {
|
|
localStorage.setItem(CITY_STORAGE_KEY, resolved.cityCode);
|
|
}
|
|
}
|
|
return resolved;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function syncCityCodeFromGps(resolved: ResolvedUserCity) {
|
|
if (resolved.openCity && resolved.cityCode) {
|
|
localStorage.setItem(CITY_STORAGE_KEY, resolved.cityCode);
|
|
}
|
|
}
|