|
|
|
|
@ -1,21 +1,157 @@
|
|
|
|
|
/**
|
|
|
|
|
* GPS定位模块
|
|
|
|
|
* 优先使用高德地图定位,fallback到Capacitor原生定位
|
|
|
|
|
* GPS定位模块 + 高德地图动态显示
|
|
|
|
|
*
|
|
|
|
|
* 地图显示方案:
|
|
|
|
|
* 1. 优先使用高德JS动态地图(支持缩放、拖动、标记)
|
|
|
|
|
* 2. 动态地图失败时fallback到静态地图图片
|
|
|
|
|
*
|
|
|
|
|
* 关键修复(基于搜索结果):
|
|
|
|
|
* - 使用AMapLoader异步加载JS API,确保完全加载后再初始化
|
|
|
|
|
* - 确保地图容器有明确宽高(SPA页面切换常见问题)
|
|
|
|
|
* - AndroidManifest.xml添加usesCleartextTraffic
|
|
|
|
|
* - 页面可见后再初始化地图
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const LocationModule = (() => {
|
|
|
|
|
let lastPosition = null;
|
|
|
|
|
let watchId = null;
|
|
|
|
|
let reportTimer = null;
|
|
|
|
|
let isReporting = false;
|
|
|
|
|
let amapKeyValid = null; // null=未检测, true=有效, false=无效
|
|
|
|
|
let amapLoaded = false; // JS API是否加载完成
|
|
|
|
|
let amapLoading = false; // 是否正在加载
|
|
|
|
|
let currentMap = null; // 当前地图实例
|
|
|
|
|
const AMAP_KEY = 'c014127be1ea5a1efead8419c94fbaba';
|
|
|
|
|
|
|
|
|
|
// 检查是否在Capacitor环境
|
|
|
|
|
// ========== 高德JS API异步加载 ==========
|
|
|
|
|
// 高德JS API异步加载(关键:使用callback参数确保完全加载)
|
|
|
|
|
function loadAmapScript() {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
// 已经加载过
|
|
|
|
|
if (typeof AMap !== 'undefined' && AMap.Map) {
|
|
|
|
|
amapLoaded = true;
|
|
|
|
|
resolve(AMap);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 正在加载中,等待
|
|
|
|
|
if (amapLoading) {
|
|
|
|
|
const checkInterval = setInterval(() => {
|
|
|
|
|
if (typeof AMap !== 'undefined' && AMap.Map) {
|
|
|
|
|
clearInterval(checkInterval);
|
|
|
|
|
amapLoaded = true;
|
|
|
|
|
resolve(AMap);
|
|
|
|
|
}
|
|
|
|
|
}, 200);
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
clearInterval(checkInterval);
|
|
|
|
|
reject(new Error('高德JS加载超时'));
|
|
|
|
|
}, 15000);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
amapLoading = true;
|
|
|
|
|
|
|
|
|
|
// 关键修复:使用callback参数!高德JS API 2.0必须通过callback通知加载完成
|
|
|
|
|
window._amapCallback = function() {
|
|
|
|
|
amapLoaded = true;
|
|
|
|
|
amapLoading = false;
|
|
|
|
|
if (typeof AMap !== 'undefined') {
|
|
|
|
|
resolve(AMap);
|
|
|
|
|
} else {
|
|
|
|
|
reject(new Error('AMap未定义'));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const script = document.createElement('script');
|
|
|
|
|
script.type = 'text/javascript';
|
|
|
|
|
script.charset = 'utf-8';
|
|
|
|
|
script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}&callback=_amapCallback&plugin=AMap.Geolocation,AMap.Scale,AMap.Marker,AMap.Geocoder,AMap.PlaceSearch`;
|
|
|
|
|
script.onerror = () => {
|
|
|
|
|
amapLoading = false;
|
|
|
|
|
reject(new Error('高德JS加载失败'));
|
|
|
|
|
};
|
|
|
|
|
document.head.appendChild(script);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 动态地图初始化 ==========
|
|
|
|
|
async function initDynamicMap(containerId, lat, lng) {
|
|
|
|
|
try {
|
|
|
|
|
const AMap = await loadAmapScript();
|
|
|
|
|
const container = document.getElementById(containerId);
|
|
|
|
|
if (!container) throw new Error('地图容器不存在');
|
|
|
|
|
|
|
|
|
|
// 关键修复:确保容器有明确宽高
|
|
|
|
|
container.style.width = '100%';
|
|
|
|
|
container.style.height = '200px';
|
|
|
|
|
container.style.minHeight = '200px';
|
|
|
|
|
container.style.display = 'block';
|
|
|
|
|
container.style.border = 'none';
|
|
|
|
|
container.style.background = '#f5f5f5';
|
|
|
|
|
|
|
|
|
|
// 销毁旧地图
|
|
|
|
|
if (currentMap) {
|
|
|
|
|
currentMap.destroy();
|
|
|
|
|
currentMap = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初始化地图
|
|
|
|
|
currentMap = new AMap.Map(containerId, {
|
|
|
|
|
zoom: 15,
|
|
|
|
|
center: [lng, lat],
|
|
|
|
|
viewMode: '2D',
|
|
|
|
|
resizeEnable: true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 添加标记
|
|
|
|
|
new AMap.Marker({
|
|
|
|
|
position: [lng, lat],
|
|
|
|
|
map: currentMap,
|
|
|
|
|
title: '当前位置'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 添加缩放控件
|
|
|
|
|
currentMap.addControl(new AMap.Scale());
|
|
|
|
|
|
|
|
|
|
return currentMap;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('动态地图初始化失败:', e);
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 静态地图图片(fallback)==========
|
|
|
|
|
function showStaticMap(containerId, lat, lng) {
|
|
|
|
|
const container = document.getElementById(containerId);
|
|
|
|
|
if (!container) return;
|
|
|
|
|
const w = container.clientWidth || 350;
|
|
|
|
|
const h = 200;
|
|
|
|
|
const imgUrl = `https://restapi.amap.com/v3/staticmap?location=${lng},${lat}&zoom=15&size=${w}*${h}&markers=mid,,A:${lng},${lat}&key=${AMAP_KEY}`;
|
|
|
|
|
container.innerHTML = `<img src="${imgUrl}" style="width:100%;height:${h}px;border-radius:12px;object-fit:cover;" onerror="this.parentElement.innerHTML='<div style=\'text-align:center;color:#999;padding:60px 20px;\'>🗺️<br>地图加载失败</div>'">`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 统一地图显示入口 ==========
|
|
|
|
|
async function showMap(containerId, lat, lng) {
|
|
|
|
|
const container = document.getElementById(containerId);
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
// 先清空容器
|
|
|
|
|
container.innerHTML = '<div style="text-align:center;color:#999;padding:60px 20px;">🗺️<br>地图加载中...</div>';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 尝试动态地图
|
|
|
|
|
await initDynamicMap(containerId, lat, lng);
|
|
|
|
|
console.log('✅ 动态地图加载成功');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.log('动态地图失败,使用静态地图:', e.message);
|
|
|
|
|
// fallback到静态地图
|
|
|
|
|
showStaticMap(containerId, lat, lng);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 定位相关 ==========
|
|
|
|
|
function isCapacitor() {
|
|
|
|
|
return typeof Capacitor !== 'undefined' && Capacitor.isNativePlatform && Capacitor.isNativePlatform();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取Geolocation插件
|
|
|
|
|
function getGeolocation() {
|
|
|
|
|
if (isCapacitor() && Capacitor.Plugins && Capacitor.Plugins.Geolocation) {
|
|
|
|
|
return Capacitor.Plugins.Geolocation;
|
|
|
|
|
@ -23,23 +159,11 @@ const LocationModule = (() => {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查高德Key是否有效
|
|
|
|
|
function checkAmapKey() {
|
|
|
|
|
const script = document.querySelector('script[src*="webapi.amap.com"]');
|
|
|
|
|
if (!script) return false;
|
|
|
|
|
const src = script.getAttribute('src');
|
|
|
|
|
return src && !src.includes('YOUR_AMAP_KEY');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 高德地图定位
|
|
|
|
|
// 高德定位
|
|
|
|
|
async function getAmapPosition() {
|
|
|
|
|
const AMap = await loadAmapScript();
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
if (typeof AMap === 'undefined') {
|
|
|
|
|
reject(new Error('高德JS API未加载'));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const geolocation = new AMap.Geolocation({
|
|
|
|
|
const geo = new AMap.Geolocation({
|
|
|
|
|
enableHighAccuracy: true,
|
|
|
|
|
timeout: 15000,
|
|
|
|
|
showButton: false,
|
|
|
|
|
@ -48,220 +172,393 @@ const LocationModule = (() => {
|
|
|
|
|
panToLocation: false,
|
|
|
|
|
zoomToAccuracy: false
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
geolocation.getCurrentPosition((status, result) => {
|
|
|
|
|
geo.getCurrentPosition((status, result) => {
|
|
|
|
|
if (status === 'complete' && result.position) {
|
|
|
|
|
resolve({
|
|
|
|
|
lat: result.position.lat,
|
|
|
|
|
lng: result.position.lng,
|
|
|
|
|
accuracy: result.accuracy || 50,
|
|
|
|
|
altitude: null,
|
|
|
|
|
speed: null,
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
source: 'amap'
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
reject(new Error('高德定位失败: ' + (result.message || '未知错误')));
|
|
|
|
|
reject(new Error(result.message || '高德定位失败'));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查定位权限
|
|
|
|
|
async function checkPermission() {
|
|
|
|
|
if (!isCapacitor()) {
|
|
|
|
|
return { location: 'granted' };
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const geo = getGeolocation();
|
|
|
|
|
if (geo && geo.requestPermissions) {
|
|
|
|
|
const perm = await geo.requestPermissions();
|
|
|
|
|
return perm;
|
|
|
|
|
// Capacitor原生定位
|
|
|
|
|
async function getCapacitorPosition() {
|
|
|
|
|
const geo = getGeolocation();
|
|
|
|
|
if (!geo) throw new Error('Capacitor Geolocation不可用');
|
|
|
|
|
const result = await geo.getCurrentPosition({
|
|
|
|
|
enableHighAccuracy: true,
|
|
|
|
|
timeout: 15000,
|
|
|
|
|
enableLocationFallback: true
|
|
|
|
|
});
|
|
|
|
|
return {
|
|
|
|
|
lat: result.coords.latitude,
|
|
|
|
|
lng: result.coords.longitude,
|
|
|
|
|
accuracy: result.coords.accuracy,
|
|
|
|
|
source: 'native'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 浏览器定位
|
|
|
|
|
async function getBrowserPosition() {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
if (!navigator.geolocation) {
|
|
|
|
|
reject(new Error('浏览器不支持定位'));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
|
|
|
(result) => {
|
|
|
|
|
resolve({
|
|
|
|
|
lat: result.coords.latitude,
|
|
|
|
|
lng: result.coords.longitude,
|
|
|
|
|
accuracy: result.coords.accuracy,
|
|
|
|
|
source: 'browser'
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
(err) => reject(new Error(err.message)),
|
|
|
|
|
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IP定位
|
|
|
|
|
async function getIpPosition() {
|
|
|
|
|
const services = [
|
|
|
|
|
{ url: 'https://ipapi.co/json/', parse: (d) => ({ lat: d.latitude, lng: d.longitude, city: d.city }) },
|
|
|
|
|
{ url: 'https://ip-api.com/json/', parse: (d) => ({ lat: d.lat, lng: d.lon, city: d.city }) }
|
|
|
|
|
];
|
|
|
|
|
for (const svc of services) {
|
|
|
|
|
try {
|
|
|
|
|
const controller = new AbortController();
|
|
|
|
|
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
|
|
|
|
const resp = await fetch(svc.url, { signal: controller.signal });
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
const data = await resp.json();
|
|
|
|
|
const parsed = svc.parse(data);
|
|
|
|
|
if (parsed.lat && parsed.lng) {
|
|
|
|
|
return {
|
|
|
|
|
lat: parsed.lat,
|
|
|
|
|
lng: parsed.lng,
|
|
|
|
|
accuracy: 5000,
|
|
|
|
|
source: 'ip',
|
|
|
|
|
city: parsed.city
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.log('IP定位失败:', svc.url);
|
|
|
|
|
}
|
|
|
|
|
return { location: 'granted' };
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('权限检查失败:', e);
|
|
|
|
|
return { location: 'denied' };
|
|
|
|
|
}
|
|
|
|
|
throw new Error('IP定位失败');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 原生GPS定位
|
|
|
|
|
async function getNativePosition() {
|
|
|
|
|
const geo = getGeolocation();
|
|
|
|
|
let pos;
|
|
|
|
|
// 统一入口
|
|
|
|
|
async function getCurrentPosition() {
|
|
|
|
|
const errors = [];
|
|
|
|
|
|
|
|
|
|
if (geo) {
|
|
|
|
|
const result = await geo.getCurrentPosition({
|
|
|
|
|
enableHighAccuracy: true,
|
|
|
|
|
timeout: 10000
|
|
|
|
|
// 高德定位
|
|
|
|
|
try {
|
|
|
|
|
const pos = await getAmapPosition();
|
|
|
|
|
lastPosition = pos;
|
|
|
|
|
console.log('✅ 高德定位:', pos.lat.toFixed(4), pos.lng.toFixed(4));
|
|
|
|
|
return pos;
|
|
|
|
|
} catch (e) { errors.push('高德:' + e.message); }
|
|
|
|
|
|
|
|
|
|
// 原生定位
|
|
|
|
|
try {
|
|
|
|
|
const pos = await getCapacitorPosition();
|
|
|
|
|
lastPosition = pos;
|
|
|
|
|
console.log('✅ 原生定位:', pos.lat.toFixed(4), pos.lng.toFixed(4));
|
|
|
|
|
return pos;
|
|
|
|
|
} catch (e) { errors.push('原生:' + e.message); }
|
|
|
|
|
|
|
|
|
|
// 浏览器定位
|
|
|
|
|
try {
|
|
|
|
|
const pos = await getBrowserPosition();
|
|
|
|
|
lastPosition = pos;
|
|
|
|
|
console.log('✅ 浏览器定位:', pos.lat.toFixed(4), pos.lng.toFixed(4));
|
|
|
|
|
return pos;
|
|
|
|
|
} catch (e) { errors.push('浏览器:' + e.message); }
|
|
|
|
|
|
|
|
|
|
// IP定位
|
|
|
|
|
try {
|
|
|
|
|
const pos = await getIpPosition();
|
|
|
|
|
lastPosition = pos;
|
|
|
|
|
console.log('✅ IP定位:', pos.lat.toFixed(4), pos.lng.toFixed(4), pos.city);
|
|
|
|
|
return pos;
|
|
|
|
|
} catch (e) { errors.push('IP:' + e.message); }
|
|
|
|
|
|
|
|
|
|
// 默认
|
|
|
|
|
console.error('❌ 全部失败:', errors.join('; '));
|
|
|
|
|
lastPosition = { lat: 30.2500, lng: 120.1600, accuracy: 100, source: 'default' };
|
|
|
|
|
return lastPosition;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== 地图选点功能 ==========
|
|
|
|
|
let pickerMap = null;
|
|
|
|
|
let pickerMarker = null;
|
|
|
|
|
let pickerGeocoder = null;
|
|
|
|
|
|
|
|
|
|
// 初始化选点地图
|
|
|
|
|
async function initPickerMap(containerId, onSelectCallback) {
|
|
|
|
|
try {
|
|
|
|
|
console.log('开始加载高德JS API...');
|
|
|
|
|
const AMap = await loadAmapScript();
|
|
|
|
|
console.log('高德JS API加载完成');
|
|
|
|
|
|
|
|
|
|
const container = document.getElementById(containerId);
|
|
|
|
|
if (!container) {
|
|
|
|
|
console.error('地图容器不存在:', containerId);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 关键:确保容器可见且有明确尺寸
|
|
|
|
|
container.style.width = '100%';
|
|
|
|
|
container.style.height = '280px';
|
|
|
|
|
container.style.minHeight = '280px';
|
|
|
|
|
container.style.display = 'block';
|
|
|
|
|
container.style.position = 'relative';
|
|
|
|
|
container.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
// 检查容器尺寸
|
|
|
|
|
const rect = container.getBoundingClientRect();
|
|
|
|
|
console.log('容器尺寸:', rect.width, 'x', rect.height);
|
|
|
|
|
if (rect.width === 0 || rect.height === 0) {
|
|
|
|
|
console.warn('容器尺寸为0,延迟初始化');
|
|
|
|
|
// 如果尺寸为0,延迟100ms再试
|
|
|
|
|
await new Promise(r => setTimeout(r, 300));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 销毁旧地图
|
|
|
|
|
if (pickerMap) {
|
|
|
|
|
pickerMap.destroy();
|
|
|
|
|
pickerMap = null;
|
|
|
|
|
pickerMarker = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取当前位置作为中心点
|
|
|
|
|
const center = lastPosition || { lat: 30.2500, lng: 120.1600 };
|
|
|
|
|
console.log('地图中心:', center.lng, center.lat);
|
|
|
|
|
|
|
|
|
|
// 初始化地图
|
|
|
|
|
pickerMap = new AMap.Map(containerId, {
|
|
|
|
|
zoom: 15,
|
|
|
|
|
center: [center.lng, center.lat],
|
|
|
|
|
viewMode: '2D',
|
|
|
|
|
resizeEnable: true
|
|
|
|
|
});
|
|
|
|
|
pos = {
|
|
|
|
|
lat: result.coords.latitude,
|
|
|
|
|
lng: result.coords.longitude,
|
|
|
|
|
accuracy: result.coords.accuracy,
|
|
|
|
|
altitude: result.coords.altitude,
|
|
|
|
|
speed: result.coords.speed,
|
|
|
|
|
timestamp: result.timestamp,
|
|
|
|
|
source: 'native'
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
pos = await new Promise((resolve, reject) => {
|
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
|
|
|
(result) => {
|
|
|
|
|
resolve({
|
|
|
|
|
lat: result.coords.latitude,
|
|
|
|
|
lng: result.coords.longitude,
|
|
|
|
|
accuracy: result.coords.accuracy,
|
|
|
|
|
altitude: result.coords.altitude,
|
|
|
|
|
speed: result.coords.speed,
|
|
|
|
|
timestamp: result.timestamp,
|
|
|
|
|
source: 'browser'
|
|
|
|
|
|
|
|
|
|
console.log('地图实例创建成功');
|
|
|
|
|
|
|
|
|
|
// 等待地图加载完成
|
|
|
|
|
pickerMap.on('complete', () => {
|
|
|
|
|
console.log('地图加载完成');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 添加当前位置标记(蓝色)
|
|
|
|
|
new AMap.Marker({
|
|
|
|
|
position: [center.lng, center.lat],
|
|
|
|
|
map: pickerMap,
|
|
|
|
|
title: '当前位置',
|
|
|
|
|
icon: new AMap.Icon({
|
|
|
|
|
size: new AMap.Size(25, 34),
|
|
|
|
|
image: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png',
|
|
|
|
|
imageSize: new AMap.Size(25, 34)
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 初始化地理编码插件
|
|
|
|
|
pickerGeocoder = new AMap.Geocoder({
|
|
|
|
|
radius: 1000,
|
|
|
|
|
extensions: 'all'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 绑定点击事件
|
|
|
|
|
pickerMap.on('click', (e) => {
|
|
|
|
|
const lng = e.lnglat.lng;
|
|
|
|
|
const lat = e.lnglat.lat;
|
|
|
|
|
console.log('地图点击:', lat, lng);
|
|
|
|
|
|
|
|
|
|
// 清除旧标记
|
|
|
|
|
if (pickerMarker) {
|
|
|
|
|
pickerMarker.setMap(null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加新标记(红色)
|
|
|
|
|
pickerMarker = new AMap.Marker({
|
|
|
|
|
position: [lng, lat],
|
|
|
|
|
map: pickerMap,
|
|
|
|
|
title: '投放点',
|
|
|
|
|
animation: 'AMAP_ANIMATION_DROP',
|
|
|
|
|
icon: new AMap.Icon({
|
|
|
|
|
size: new AMap.Size(25, 34),
|
|
|
|
|
image: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_r.png',
|
|
|
|
|
imageSize: new AMap.Size(25, 34)
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 逆地理编码获取地址
|
|
|
|
|
pickerGeocoder.getAddress([lng, lat], (status, result) => {
|
|
|
|
|
let address = '';
|
|
|
|
|
let name = '';
|
|
|
|
|
if (status === 'complete' && result.regeocode) {
|
|
|
|
|
address = result.regeocode.formattedAddress;
|
|
|
|
|
const comp = result.regeocode.addressComponent;
|
|
|
|
|
name = comp.building || comp.street || comp.township || '选定位置';
|
|
|
|
|
}
|
|
|
|
|
if (onSelectCallback) {
|
|
|
|
|
onSelectCallback({
|
|
|
|
|
lat, lng,
|
|
|
|
|
name: name || '选定位置',
|
|
|
|
|
address: address || `${lat.toFixed(6)}, ${lng.toFixed(6)}`
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
(err) => reject(err),
|
|
|
|
|
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
pickerMap.setCenter([lng, lat]);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return pos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初始化高德地图(在位置更新页面显示)
|
|
|
|
|
function initAmap(containerId, lat, lng) {
|
|
|
|
|
if (typeof AMap === 'undefined') {
|
|
|
|
|
console.log('高德JS API未加载,无法显示地图');
|
|
|
|
|
pickerMap.addControl(new AMap.Scale());
|
|
|
|
|
console.log('选点地图初始化成功');
|
|
|
|
|
return pickerMap;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('选点地图初始化失败:', e);
|
|
|
|
|
const center = lastPosition || { lat: 30.2500, lng: 120.1600 };
|
|
|
|
|
showStaticMap(containerId, center.lat, center.lng);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 搜索地点
|
|
|
|
|
async function searchPlace(keyword, callback) {
|
|
|
|
|
try {
|
|
|
|
|
const map = new AMap.Map(containerId, {
|
|
|
|
|
zoom: 15,
|
|
|
|
|
center: [lng, lat],
|
|
|
|
|
viewMode: '2D'
|
|
|
|
|
const AMap = await loadAmapScript();
|
|
|
|
|
const placeSearch = new AMap.PlaceSearch({
|
|
|
|
|
pageSize: 5,
|
|
|
|
|
pageIndex: 1,
|
|
|
|
|
extensions: 'all'
|
|
|
|
|
});
|
|
|
|
|
new AMap.Marker({
|
|
|
|
|
position: [lng, lat],
|
|
|
|
|
map: map
|
|
|
|
|
placeSearch.search(keyword, (status, result) => {
|
|
|
|
|
if (status === 'complete' && result.info === 'OK') {
|
|
|
|
|
const pois = result.poiList.pois.map(poi => ({
|
|
|
|
|
name: poi.name,
|
|
|
|
|
address: poi.address,
|
|
|
|
|
lat: poi.location.lat,
|
|
|
|
|
lng: poi.location.lng,
|
|
|
|
|
type: poi.type
|
|
|
|
|
}));
|
|
|
|
|
callback(null, pois);
|
|
|
|
|
} else {
|
|
|
|
|
callback(new Error('未找到相关地点'), []);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return map;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('高德地图初始化失败:', e);
|
|
|
|
|
return null;
|
|
|
|
|
callback(e, []);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取当前位置(优先高德,fallback原生)
|
|
|
|
|
async function getCurrentPosition() {
|
|
|
|
|
// 检测高德Key
|
|
|
|
|
if (amapKeyValid === null) {
|
|
|
|
|
amapKeyValid = checkAmapKey();
|
|
|
|
|
// 在选点地图上定位到指定坐标
|
|
|
|
|
async function setPickerPosition(lat, lng, name, address) {
|
|
|
|
|
if (!pickerMap) return;
|
|
|
|
|
try {
|
|
|
|
|
const AMap = await loadAmapScript();
|
|
|
|
|
if (pickerMarker) pickerMarker.setMap(null);
|
|
|
|
|
pickerMarker = new AMap.Marker({
|
|
|
|
|
position: [lng, lat],
|
|
|
|
|
map: pickerMap,
|
|
|
|
|
title: name || '投放点',
|
|
|
|
|
animation: 'AMAP_ANIMATION_DROP',
|
|
|
|
|
icon: new AMap.Icon({
|
|
|
|
|
size: new AMap.Size(25, 34),
|
|
|
|
|
image: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_r.png',
|
|
|
|
|
imageSize: new AMap.Size(25, 34)
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
pickerMap.setCenter([lng, lat]);
|
|
|
|
|
pickerMap.setZoom(16);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('设置选点位置失败:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 优先尝试高德定位
|
|
|
|
|
if (amapKeyValid) {
|
|
|
|
|
// 定位失败原因
|
|
|
|
|
async function getLocationErrorReason() {
|
|
|
|
|
const reasons = [];
|
|
|
|
|
if (isCapacitor()) {
|
|
|
|
|
try {
|
|
|
|
|
const pos = await getAmapPosition();
|
|
|
|
|
lastPosition = pos;
|
|
|
|
|
console.log('高德定位成功:', pos.lat.toFixed(6), pos.lng.toFixed(6));
|
|
|
|
|
return pos;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.log('高德定位失败,尝试原生定位:', e.message);
|
|
|
|
|
}
|
|
|
|
|
const geo = getGeolocation();
|
|
|
|
|
if (geo && geo.checkPermissions) {
|
|
|
|
|
const perm = await geo.checkPermissions();
|
|
|
|
|
if (perm.location !== 'granted') reasons.push('App定位权限未授予');
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback到原生定位
|
|
|
|
|
try {
|
|
|
|
|
const pos = await getNativePosition();
|
|
|
|
|
lastPosition = pos;
|
|
|
|
|
console.log('原生定位成功:', pos.lat.toFixed(6), pos.lng.toFixed(6));
|
|
|
|
|
return pos;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('原生定位也失败:', e);
|
|
|
|
|
// 返回默认位置(杭州附近)
|
|
|
|
|
lastPosition = { lat: 30.2500, lng: 120.1600, accuracy: 100, source: 'default' };
|
|
|
|
|
return lastPosition;
|
|
|
|
|
if (!navigator.onLine) reasons.push('设备未连接网络');
|
|
|
|
|
if (reasons.length === 0) {
|
|
|
|
|
reasons.push('GPS信号弱(请靠近窗户或到室外)');
|
|
|
|
|
reasons.push('WiFi未开启(vivo需要WiFi辅助定位)');
|
|
|
|
|
}
|
|
|
|
|
return reasons;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 开始持续定位并上报
|
|
|
|
|
// 上报
|
|
|
|
|
async function startReporting(soldierId, name, intervalMs = 10000) {
|
|
|
|
|
if (isReporting) return;
|
|
|
|
|
isReporting = true;
|
|
|
|
|
|
|
|
|
|
if (reportTimer) clearInterval(reportTimer);
|
|
|
|
|
|
|
|
|
|
// 立即上报一次
|
|
|
|
|
await reportOnce(soldierId, name);
|
|
|
|
|
|
|
|
|
|
// 定时上报
|
|
|
|
|
reportTimer = setInterval(async () => {
|
|
|
|
|
if (!isReporting) return;
|
|
|
|
|
await reportOnce(soldierId, name);
|
|
|
|
|
}, intervalMs);
|
|
|
|
|
|
|
|
|
|
console.log('位置自动上报已启动,间隔:', intervalMs, 'ms');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 单次上报
|
|
|
|
|
async function reportOnce(soldierId, name) {
|
|
|
|
|
try {
|
|
|
|
|
const pos = await getCurrentPosition();
|
|
|
|
|
await API.updateLocation({
|
|
|
|
|
id: soldierId,
|
|
|
|
|
name: name,
|
|
|
|
|
lat: pos.lat,
|
|
|
|
|
lng: pos.lng
|
|
|
|
|
});
|
|
|
|
|
console.log('位置已上报:', pos.lat.toFixed(6), pos.lng.toFixed(6), '来源:', pos.source || 'unknown');
|
|
|
|
|
await API.updateLocation({ id: soldierId, name: name, lat: pos.lat, lng: pos.lng });
|
|
|
|
|
return pos;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('位置上报失败:', e);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 停止上报
|
|
|
|
|
function stopReporting() {
|
|
|
|
|
isReporting = false;
|
|
|
|
|
if (reportTimer) {
|
|
|
|
|
clearInterval(reportTimer);
|
|
|
|
|
reportTimer = null;
|
|
|
|
|
}
|
|
|
|
|
console.log('位置自动上报已停止');
|
|
|
|
|
if (reportTimer) { clearInterval(reportTimer); reportTimer = null; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取最后已知位置
|
|
|
|
|
function getLastPosition() {
|
|
|
|
|
return lastPosition;
|
|
|
|
|
}
|
|
|
|
|
// 工具
|
|
|
|
|
function getLastPosition() { return lastPosition; }
|
|
|
|
|
|
|
|
|
|
// 计算两点距离(米)
|
|
|
|
|
function calcDistance(lat1, lng1, lat2, lng2) {
|
|
|
|
|
const R = 6371000;
|
|
|
|
|
const dLat = (lat2 - lat1) * Math.PI / 180;
|
|
|
|
|
const dLng = (lng2 - lng1) * Math.PI / 180;
|
|
|
|
|
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
|
|
|
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
|
|
|
|
|
Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
|
|
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
|
|
|
return R * c;
|
|
|
|
|
const a = Math.sin(dLat/2)**2 + Math.cos(lat1 * Math.PI/180) * Math.cos(lat2 * Math.PI/180) * Math.sin(dLng/2)**2;
|
|
|
|
|
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 格式化精度显示
|
|
|
|
|
function formatAccuracy(acc) {
|
|
|
|
|
if (!acc || acc <= 0) return '未知';
|
|
|
|
|
if (acc < 10) return '极高 (' + Math.round(acc) + 'm)';
|
|
|
|
|
if (acc < 50) return '高 (' + Math.round(acc) + 'm)';
|
|
|
|
|
if (acc < 100) return '中 (' + Math.round(acc) + 'm)';
|
|
|
|
|
if (acc < 200) return '中 (' + Math.round(acc) + 'm)';
|
|
|
|
|
return '低 (' + Math.round(acc) + 'm)';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取定位来源描述
|
|
|
|
|
function getSourceText(source) {
|
|
|
|
|
const map = {
|
|
|
|
|
'amap': '高德定位',
|
|
|
|
|
'native': 'GPS定位',
|
|
|
|
|
'browser': '浏览器定位',
|
|
|
|
|
'default': '默认位置'
|
|
|
|
|
'ip': '网络定位',
|
|
|
|
|
'default': '默认位置',
|
|
|
|
|
'manual': '手动设置'
|
|
|
|
|
};
|
|
|
|
|
return map[source] || '未知';
|
|
|
|
|
}
|
|
|
|
|
@ -272,11 +569,12 @@ const LocationModule = (() => {
|
|
|
|
|
stopReporting,
|
|
|
|
|
getLastPosition,
|
|
|
|
|
calcDistance,
|
|
|
|
|
isCapacitor,
|
|
|
|
|
checkPermission,
|
|
|
|
|
formatAccuracy,
|
|
|
|
|
getSourceText,
|
|
|
|
|
checkAmapKey,
|
|
|
|
|
initAmap
|
|
|
|
|
showMap,
|
|
|
|
|
getLocationErrorReason,
|
|
|
|
|
initPickerMap,
|
|
|
|
|
searchPlace,
|
|
|
|
|
setPickerPosition
|
|
|
|
|
};
|
|
|
|
|
})();
|
|
|
|
|
|