更新单兵终端APP

zhaochang_branch
赵昌 2 days ago
parent 8f2f100cb1
commit c80538a00e

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">

@ -7,7 +7,8 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"

@ -176,19 +176,53 @@
</div>
</div>
<!-- ===== 投放点选择 ===== -->
<!-- ===== 投放点选择(地图选点) ===== -->
<div class="page" id="page-drop">
<div class="page-header">
<span class="back-btn" onclick="App.back()">← 返回</span>
<span class="page-title">🎯 选择投放</span>
<span class="page-title">🎯 地图选点</span>
<span></span>
</div>
<div class="page-content">
<div class="section-title">🎯 推荐投放点列表</div>
<div id="drop-point-list">
<!-- 动态加载 -->
<div class="page-content" style="padding: 0;">
<!-- 搜索框 -->
<div style="padding: 10px 14px; background: #fff; border-bottom: 1px solid #eee;">
<div style="display: flex; gap: 8px;">
<input type="text" id="drop-search-input" placeholder="🔍 搜索地点(如:长沙火车站)"
style="flex: 1; padding: 10px 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; outline: none;">
<button onclick="App.searchDropPoint()"
style="padding: 10px 16px; background: #007AFF; color: white; border: none; border-radius: 8px; font-size: 14px;">搜索</button>
</div>
<!-- 搜索结果列表 -->
<div id="drop-search-results" style="margin-top: 8px; max-height: 150px; overflow-y: auto;"></div>
</div>
<!-- 地图容器 -->
<div id="drop-map" style="width: 100%; height: 280px; min-height: 280px; background: #f5f5f5; display: block; position: relative;">
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #999; text-align: center; z-index: 1;">
<div style="font-size: 28px;">🗺️</div>
<div style="font-size: 13px;">点击地图选择投放点</div>
</div>
</div>
<!-- 选中位置信息 -->
<div style="padding: 12px 14px; background: #fff;">
<div style="font-size: 13px; color: #666; margin-bottom: 6px;">📍 已选位置</div>
<div id="drop-selected-name" style="font-size: 15px; font-weight: bold; color: #333; margin-bottom: 4px;">请点击地图选择投放点</div>
<div id="drop-selected-address" style="font-size: 12px; color: #999;">--</div>
<div id="drop-selected-coords" style="font-size: 12px; color: #007AFF; margin-top: 4px;">--</div>
</div>
<!-- 推荐列表(保留作为参考) -->
<div style="padding: 10px 14px;">
<div class="section-title">🎯 附近推荐投放点</div>
<div id="drop-point-list">
<!-- 动态加载 -->
</div>
</div>
<div style="padding: 10px 14px 20px;">
<button class="button-large success" onclick="App.confirmDropPoint()">✅ 确认选择此位置</button>
</div>
<button class="button-large success" onclick="App.confirmDropPoint()">✅ 确认选择</button>
</div>
</div>
@ -303,7 +337,7 @@
<div class="btn-row">
<button class="btn-primary flex-1" onclick="App.updateDropPoint()">✅ 更新投放点</button>
<button class="btn-secondary flex-1">前往原投放点</button>
<button class="btn-secondary flex-1" onclick="App.manualSetLocation()">📍 手动修正位置</button>
</div>
</div>
</div>

@ -17,6 +17,7 @@ const App = (() => {
let currentPage = 'home';
let pageStack = ['home'];
let selectedDropPoint = null;
let mapSelectedPoint = null; // 地图选中的投放点
let pollTimer = null;
// ===== 页面映射用于Tab导航显示控制 =====
@ -128,7 +129,8 @@ const App = (() => {
updateHomeLocation();
break;
case 'drop':
loadDropPoints();
// 延迟初始化,确保页面完全显示后再加载地图
setTimeout(() => loadDropPoints(), 500);
break;
case 'task':
loadTaskInfo();
@ -344,8 +346,24 @@ const App = (() => {
}
}
// ===== 加载投放点 =====
// ===== 加载投放点(含地图选点) =====
async function loadDropPoints() {
// 初始化地图选点
setTimeout(async () => {
await LocationModule.initPickerMap('drop-map', (point) => {
mapSelectedPoint = point;
// 更新UI显示
const nameEl = document.getElementById('drop-selected-name');
const addrEl = document.getElementById('drop-selected-address');
const coordEl = document.getElementById('drop-selected-coords');
if (nameEl) nameEl.textContent = point.name;
if (addrEl) addrEl.textContent = point.address;
if (coordEl) coordEl.textContent = `${point.lat.toFixed(6)}, ${point.lng.toFixed(6)}`;
showToast('📍 已选择:' + point.name);
});
}, 300);
// 加载推荐列表
const list = document.getElementById('drop-point-list');
list.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">加载中...</div>';
@ -359,16 +377,16 @@ const App = (() => {
list.innerHTML = points.map((p, i) => {
const isSafe = p.safety_score >= 70;
return `
<div class="card drop-card ${isSafe ? 'safe' : 'danger'}">
<div class="card drop-card ${isSafe ? 'safe' : 'danger'}" onclick="App.selectDropPoint(${i})" style="cursor:pointer;">
<div style="font-size:14px;color:#333;margin-bottom:5px;">📍 ${p.name}</div>
<div class="drop-info">
<span>安全系数: ${p.safety_score}%</span>
<span>距离: ${p.distance}m</span>
</div>
<div style="font-size:12px;color:#666;margin-bottom:10px;">${p.reason}</div>
<button class="${isSafe ? 'select-btn' : 'avoid-btn'}" onclick="App.selectDropPoint(${i})">
${isSafe ? '✅ 选择此点' : '❌ 避开此点'}
</button>
<div style="font-size:12px;color:${isSafe ? '#52c41a' : '#ff4d4f'};">
${isSafe ? '✅ 推荐投放' : '❌ 危险区域'}
</div>
</div>
`;
}).join('');
@ -377,27 +395,119 @@ const App = (() => {
}
}
// ===== 选择投放点 =====
// ===== 搜索地点 =====
function searchDropPoint() {
const input = document.getElementById('drop-search-input');
const resultsDiv = document.getElementById('drop-search-results');
const keyword = input ? input.value.trim() : '';
if (!keyword) {
showToast('请输入搜索关键词');
return;
}
resultsDiv.innerHTML = '<div style="padding:8px;color:#999;font-size:12px;">搜索中...</div>';
LocationModule.searchPlace(keyword, (err, pois) => {
if (err || pois.length === 0) {
resultsDiv.innerHTML = '<div style="padding:8px;color:#999;font-size:12px;">未找到相关地点</div>';
return;
}
resultsDiv.innerHTML = pois.map((p, i) => `
<div onclick="App.selectSearchResult(${i})"
style="padding:8px 10px;border-bottom:1px solid #f0f0f0;cursor:pointer;"
data-name="${p.name}" data-address="${p.address}" data-lat="${p.lat}" data-lng="${p.lng}">
<div style="font-size:13px;color:#333;">${p.name}</div>
<div style="font-size:11px;color:#999;">${p.address}</div>
</div>
`).join('');
// 存储搜索结果供点击使用
window._searchResults = pois;
});
}
// 选择搜索结果
async function selectSearchResult(index) {
const pois = window._searchResults || [];
const p = pois[index];
if (!p) return;
// 在地图上定位
await LocationModule.setPickerPosition(p.lat, p.lng, p.name, p.address);
// 更新选中状态
mapSelectedPoint = {
lat: p.lat,
lng: p.lng,
name: p.name,
address: p.address
};
// 更新UI
const nameEl = document.getElementById('drop-selected-name');
const addrEl = document.getElementById('drop-selected-address');
const coordEl = document.getElementById('drop-selected-coords');
if (nameEl) nameEl.textContent = p.name;
if (addrEl) addrEl.textContent = p.address;
if (coordEl) coordEl.textContent = `${p.lat.toFixed(6)}, ${p.lng.toFixed(6)}`;
// 清空搜索结果
const resultsDiv = document.getElementById('drop-search-results');
if (resultsDiv) resultsDiv.innerHTML = '';
showToast('📍 已定位:' + p.name);
}
// ===== 选择投放点(从列表) =====
function selectDropPoint(index) {
API.getDropPoints().then(points => {
API.getDropPoints().then(async points => {
const p = points[index];
if (p.safety_score < 70) {
showToast('已标记避开此点');
showToast('⚠️ 该区域危险,建议选择其他投放点');
return;
}
selectedDropPoint = p;
// 同时在地图上标记
await LocationModule.setPickerPosition(p.lat, p.lng, p.name, p.address);
// 更新选中显示
mapSelectedPoint = {
lat: p.lat,
lng: p.lng,
name: p.name,
address: p.address || p.name
};
const nameEl = document.getElementById('drop-selected-name');
const addrEl = document.getElementById('drop-selected-address');
const coordEl = document.getElementById('drop-selected-coords');
if (nameEl) nameEl.textContent = p.name;
if (addrEl) addrEl.textContent = p.address || '安全系数 ' + p.safety_score + '%';
if (coordEl) coordEl.textContent = `${p.lat.toFixed(6)}, ${p.lng.toFixed(6)}`;
showToast('已选择:' + p.name);
router('demand');
document.getElementById('demand-drop-display').textContent = p.name + '(安全系数' + p.safety_score + '%';
});
}
function confirmDropPoint() {
if (!selectedDropPoint) {
showToast('请先选择一个投放点');
// 优先使用地图选中的点
if (mapSelectedPoint) {
selectedDropPoint = mapSelectedPoint;
router('demand');
const display = document.getElementById('demand-drop-display');
if (display) display.textContent = mapSelectedPoint.name;
showToast('✅ 已确认投放点:' + mapSelectedPoint.name);
return;
}
// fallback到列表选中的点
if (selectedDropPoint) {
router('demand');
const display = document.getElementById('demand-drop-display');
if (display) display.textContent = selectedDropPoint.name;
return;
}
router('demand');
showToast('请先点击地图或列表选择一个投放点');
}
// ===== 加载任务信息 =====
@ -478,30 +588,68 @@ const App = (() => {
}
}
// ===== 手动修正位置 =====
function manualSetLocation() {
const input = prompt('请输入您的坐标(格式:纬度,经度)\n示例28.2280,112.9388\n长沙大约28.2280,112.9388');
if (!input) return;
const parts = input.split(',').map(s => parseFloat(s.trim()));
if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
const pos = { lat: parts[0], lng: parts[1], accuracy: 10, source: 'manual' };
LocationModule.lastPosition = pos;
const curEl = document.getElementById('loc-current');
if (curEl) curEl.textContent = `${pos.lat.toFixed(6)}, ${pos.lng.toFixed(6)}`;
const accEl = document.getElementById('loc-accuracy');
if (accEl) accEl.textContent = '手动设置 · 精确';
updateHomeLocation();
showToast('位置已手动修正');
// 刷新地图
const mapContainer = document.getElementById('loc-map');
if (mapContainer) {
mapContainer.innerHTML = '';
setTimeout(() => LocationModule.showMap('loc-map', pos.lat, pos.lng), 100);
}
} else {
showToast('格式错误,请使用:纬度,经度');
}
}
// ===== 刷新位置 =====
async function refreshLocation() {
const curEl = document.getElementById('loc-current');
if (curEl) curEl.textContent = '定位中...';
try {
const pos = await LocationModule.getCurrentPosition();
const sourceText = LocationModule.getSourceText(pos.source);
// 更新当前位置显示
if (curEl) {
const sourceText = LocationModule.getSourceText(pos.source);
if (pos.source === 'default') {
curEl.innerHTML = `${pos.lat.toFixed(6)}, ${pos.lng.toFixed(6)} <span style="color:#fa8c16;font-size:12px;">(${sourceText})</span>`;
curEl.innerHTML = `<span style="color:#999">${pos.lat.toFixed(4)}, ${pos.lng.toFixed(4)}</span>`;
} else if (pos.source === 'ip') {
curEl.innerHTML = `${pos.lat.toFixed(4)}, ${pos.lng.toFixed(4)} <span style="color:#1890ff;font-size:12px">(${sourceText})</span>`;
} else {
curEl.textContent = `${pos.lat.toFixed(6)}, ${pos.lng.toFixed(6)}`;
}
}
// 更新精度显示
const accEl = document.getElementById('loc-accuracy');
if (accEl) {
const sourceText = LocationModule.getSourceText(pos.source);
accEl.textContent = LocationModule.formatAccuracy(pos.accuracy) + ' · ' + sourceText;
let text = LocationModule.formatAccuracy(pos.accuracy) + ' · ' + sourceText;
if (pos.source === 'default') {
text = '❌ 定位失败 · 使用默认坐标';
} else if (pos.source === 'ip') {
text = '📡 ' + LocationModule.formatAccuracy(pos.accuracy) + ' · ' + sourceText + (pos.city ? ' (' + pos.city + ')' : '');
}
accEl.textContent = text;
}
// 更新上报时间
const reportEl = document.getElementById('loc-last-report');
if (reportEl) reportEl.textContent = new Date().toTimeString().split(' ')[0];
// 更新偏移距离
const offsetEl = document.getElementById('loc-offset');
if (offsetEl) {
offsetEl.textContent = '计算中...';
@ -512,24 +660,35 @@ const App = (() => {
}, 500);
}
// 初始化高德地图
// 初始化/更新地图
const mapContainer = document.getElementById('loc-map');
if (mapContainer && pos.source !== 'default') {
mapContainer.innerHTML = '';
mapContainer.style.display = 'block';
mapContainer.style.border = 'none';
setTimeout(() => {
LocationModule.initAmap('loc-map', pos.lat, pos.lng);
}, 100);
} else if (mapContainer && pos.source === 'default') {
mapContainer.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">🚫 定位失败,无法加载地图<br><span style="font-size:12px">请检查GPS权限和定位服务</span></div>';
if (mapContainer) {
if (pos.source === 'default') {
// 定位完全失败,显示诊断信息
const reasons = await LocationModule.getLocationErrorReason();
mapContainer.innerHTML = `
<div style="text-align:center;color:#666;padding:15px;">
<div style="font-size:28px;margin-bottom:8px;">🚫</div>
<div style="font-size:14px;font-weight:bold;margin-bottom:8px;">定位失败</div>
<div style="font-size:12px;color:#999;text-align:left;display:inline-block;">
${reasons.map(r => '• ' + r).join('<br>')}
</div>
<div style="font-size:12px;color:#1890ff;margin-top:10px;">
💡 建议开启WiFi + GPS + 到窗边
</div>
</div>`;
} else {
mapContainer.innerHTML = '';
mapContainer.style.border = 'none';
setTimeout(() => LocationModule.showMap('loc-map', pos.lat, pos.lng), 100);
}
}
// 更新首页位置显示
updateHomeLocation();
} catch (e) {
if (curEl) curEl.textContent = '定位失败';
if (curEl) curEl.textContent = '定位异常';
console.error('refreshLocation错误:', e);
}
}
@ -601,10 +760,13 @@ const App = (() => {
submitDemand,
selectDropPoint,
confirmDropPoint,
searchDropPoint,
selectSearchResult,
updateDropPoint,
addAnnotate,
triggerSOS,
refreshLocation,
manualSetLocation,
showToast,
showServerConfig,
toggleSwitch,

@ -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
};
})();

@ -176,19 +176,53 @@
</div>
</div>
<!-- ===== 投放点选择 ===== -->
<!-- ===== 投放点选择(地图选点) ===== -->
<div class="page" id="page-drop">
<div class="page-header">
<span class="back-btn" onclick="App.back()">← 返回</span>
<span class="page-title">🎯 选择投放</span>
<span class="page-title">🎯 地图选点</span>
<span></span>
</div>
<div class="page-content">
<div class="section-title">🎯 推荐投放点列表</div>
<div id="drop-point-list">
<!-- 动态加载 -->
<div class="page-content" style="padding: 0;">
<!-- 搜索框 -->
<div style="padding: 10px 14px; background: #fff; border-bottom: 1px solid #eee;">
<div style="display: flex; gap: 8px;">
<input type="text" id="drop-search-input" placeholder="🔍 搜索地点(如:长沙火车站)"
style="flex: 1; padding: 10px 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; outline: none;">
<button onclick="App.searchDropPoint()"
style="padding: 10px 16px; background: #007AFF; color: white; border: none; border-radius: 8px; font-size: 14px;">搜索</button>
</div>
<!-- 搜索结果列表 -->
<div id="drop-search-results" style="margin-top: 8px; max-height: 150px; overflow-y: auto;"></div>
</div>
<!-- 地图容器 -->
<div id="drop-map" style="width: 100%; height: 280px; min-height: 280px; background: #f5f5f5; display: block; position: relative;">
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #999; text-align: center; z-index: 1;">
<div style="font-size: 28px;">🗺️</div>
<div style="font-size: 13px;">点击地图选择投放点</div>
</div>
</div>
<!-- 选中位置信息 -->
<div style="padding: 12px 14px; background: #fff;">
<div style="font-size: 13px; color: #666; margin-bottom: 6px;">📍 已选位置</div>
<div id="drop-selected-name" style="font-size: 15px; font-weight: bold; color: #333; margin-bottom: 4px;">请点击地图选择投放点</div>
<div id="drop-selected-address" style="font-size: 12px; color: #999;">--</div>
<div id="drop-selected-coords" style="font-size: 12px; color: #007AFF; margin-top: 4px;">--</div>
</div>
<!-- 推荐列表(保留作为参考) -->
<div style="padding: 10px 14px;">
<div class="section-title">🎯 附近推荐投放点</div>
<div id="drop-point-list">
<!-- 动态加载 -->
</div>
</div>
<div style="padding: 10px 14px 20px;">
<button class="button-large success" onclick="App.confirmDropPoint()">✅ 确认选择此位置</button>
</div>
<button class="button-large success" onclick="App.confirmDropPoint()">✅ 确认选择</button>
</div>
</div>
@ -303,7 +337,7 @@
<div class="btn-row">
<button class="btn-primary flex-1" onclick="App.updateDropPoint()">✅ 更新投放点</button>
<button class="btn-secondary flex-1">前往原投放点</button>
<button class="btn-secondary flex-1" onclick="App.manualSetLocation()">📍 手动修正位置</button>
</div>
</div>
</div>

@ -17,6 +17,7 @@ const App = (() => {
let currentPage = 'home';
let pageStack = ['home'];
let selectedDropPoint = null;
let mapSelectedPoint = null; // 地图选中的投放点
let pollTimer = null;
// ===== 页面映射用于Tab导航显示控制 =====
@ -128,7 +129,8 @@ const App = (() => {
updateHomeLocation();
break;
case 'drop':
loadDropPoints();
// 延迟初始化,确保页面完全显示后再加载地图
setTimeout(() => loadDropPoints(), 500);
break;
case 'task':
loadTaskInfo();
@ -344,8 +346,24 @@ const App = (() => {
}
}
// ===== 加载投放点 =====
// ===== 加载投放点(含地图选点) =====
async function loadDropPoints() {
// 初始化地图选点
setTimeout(async () => {
await LocationModule.initPickerMap('drop-map', (point) => {
mapSelectedPoint = point;
// 更新UI显示
const nameEl = document.getElementById('drop-selected-name');
const addrEl = document.getElementById('drop-selected-address');
const coordEl = document.getElementById('drop-selected-coords');
if (nameEl) nameEl.textContent = point.name;
if (addrEl) addrEl.textContent = point.address;
if (coordEl) coordEl.textContent = `${point.lat.toFixed(6)}, ${point.lng.toFixed(6)}`;
showToast('📍 已选择:' + point.name);
});
}, 300);
// 加载推荐列表
const list = document.getElementById('drop-point-list');
list.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">加载中...</div>';
@ -359,16 +377,16 @@ const App = (() => {
list.innerHTML = points.map((p, i) => {
const isSafe = p.safety_score >= 70;
return `
<div class="card drop-card ${isSafe ? 'safe' : 'danger'}">
<div class="card drop-card ${isSafe ? 'safe' : 'danger'}" onclick="App.selectDropPoint(${i})" style="cursor:pointer;">
<div style="font-size:14px;color:#333;margin-bottom:5px;">📍 ${p.name}</div>
<div class="drop-info">
<span>安全系数: ${p.safety_score}%</span>
<span>距离: ${p.distance}m</span>
</div>
<div style="font-size:12px;color:#666;margin-bottom:10px;">${p.reason}</div>
<button class="${isSafe ? 'select-btn' : 'avoid-btn'}" onclick="App.selectDropPoint(${i})">
${isSafe ? '✅ 选择此点' : '❌ 避开此点'}
</button>
<div style="font-size:12px;color:${isSafe ? '#52c41a' : '#ff4d4f'};">
${isSafe ? '✅ 推荐投放' : '❌ 危险区域'}
</div>
</div>
`;
}).join('');
@ -377,27 +395,119 @@ const App = (() => {
}
}
// ===== 选择投放点 =====
// ===== 搜索地点 =====
function searchDropPoint() {
const input = document.getElementById('drop-search-input');
const resultsDiv = document.getElementById('drop-search-results');
const keyword = input ? input.value.trim() : '';
if (!keyword) {
showToast('请输入搜索关键词');
return;
}
resultsDiv.innerHTML = '<div style="padding:8px;color:#999;font-size:12px;">搜索中...</div>';
LocationModule.searchPlace(keyword, (err, pois) => {
if (err || pois.length === 0) {
resultsDiv.innerHTML = '<div style="padding:8px;color:#999;font-size:12px;">未找到相关地点</div>';
return;
}
resultsDiv.innerHTML = pois.map((p, i) => `
<div onclick="App.selectSearchResult(${i})"
style="padding:8px 10px;border-bottom:1px solid #f0f0f0;cursor:pointer;"
data-name="${p.name}" data-address="${p.address}" data-lat="${p.lat}" data-lng="${p.lng}">
<div style="font-size:13px;color:#333;">${p.name}</div>
<div style="font-size:11px;color:#999;">${p.address}</div>
</div>
`).join('');
// 存储搜索结果供点击使用
window._searchResults = pois;
});
}
// 选择搜索结果
async function selectSearchResult(index) {
const pois = window._searchResults || [];
const p = pois[index];
if (!p) return;
// 在地图上定位
await LocationModule.setPickerPosition(p.lat, p.lng, p.name, p.address);
// 更新选中状态
mapSelectedPoint = {
lat: p.lat,
lng: p.lng,
name: p.name,
address: p.address
};
// 更新UI
const nameEl = document.getElementById('drop-selected-name');
const addrEl = document.getElementById('drop-selected-address');
const coordEl = document.getElementById('drop-selected-coords');
if (nameEl) nameEl.textContent = p.name;
if (addrEl) addrEl.textContent = p.address;
if (coordEl) coordEl.textContent = `${p.lat.toFixed(6)}, ${p.lng.toFixed(6)}`;
// 清空搜索结果
const resultsDiv = document.getElementById('drop-search-results');
if (resultsDiv) resultsDiv.innerHTML = '';
showToast('📍 已定位:' + p.name);
}
// ===== 选择投放点(从列表) =====
function selectDropPoint(index) {
API.getDropPoints().then(points => {
API.getDropPoints().then(async points => {
const p = points[index];
if (p.safety_score < 70) {
showToast('已标记避开此点');
showToast('⚠️ 该区域危险,建议选择其他投放点');
return;
}
selectedDropPoint = p;
// 同时在地图上标记
await LocationModule.setPickerPosition(p.lat, p.lng, p.name, p.address);
// 更新选中显示
mapSelectedPoint = {
lat: p.lat,
lng: p.lng,
name: p.name,
address: p.address || p.name
};
const nameEl = document.getElementById('drop-selected-name');
const addrEl = document.getElementById('drop-selected-address');
const coordEl = document.getElementById('drop-selected-coords');
if (nameEl) nameEl.textContent = p.name;
if (addrEl) addrEl.textContent = p.address || '安全系数 ' + p.safety_score + '%';
if (coordEl) coordEl.textContent = `${p.lat.toFixed(6)}, ${p.lng.toFixed(6)}`;
showToast('已选择:' + p.name);
router('demand');
document.getElementById('demand-drop-display').textContent = p.name + '(安全系数' + p.safety_score + '%';
});
}
function confirmDropPoint() {
if (!selectedDropPoint) {
showToast('请先选择一个投放点');
// 优先使用地图选中的点
if (mapSelectedPoint) {
selectedDropPoint = mapSelectedPoint;
router('demand');
const display = document.getElementById('demand-drop-display');
if (display) display.textContent = mapSelectedPoint.name;
showToast('✅ 已确认投放点:' + mapSelectedPoint.name);
return;
}
// fallback到列表选中的点
if (selectedDropPoint) {
router('demand');
const display = document.getElementById('demand-drop-display');
if (display) display.textContent = selectedDropPoint.name;
return;
}
router('demand');
showToast('请先点击地图或列表选择一个投放点');
}
// ===== 加载任务信息 =====
@ -478,30 +588,68 @@ const App = (() => {
}
}
// ===== 手动修正位置 =====
function manualSetLocation() {
const input = prompt('请输入您的坐标(格式:纬度,经度)\n示例28.2280,112.9388\n长沙大约28.2280,112.9388');
if (!input) return;
const parts = input.split(',').map(s => parseFloat(s.trim()));
if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
const pos = { lat: parts[0], lng: parts[1], accuracy: 10, source: 'manual' };
LocationModule.lastPosition = pos;
const curEl = document.getElementById('loc-current');
if (curEl) curEl.textContent = `${pos.lat.toFixed(6)}, ${pos.lng.toFixed(6)}`;
const accEl = document.getElementById('loc-accuracy');
if (accEl) accEl.textContent = '手动设置 · 精确';
updateHomeLocation();
showToast('位置已手动修正');
// 刷新地图
const mapContainer = document.getElementById('loc-map');
if (mapContainer) {
mapContainer.innerHTML = '';
setTimeout(() => LocationModule.showMap('loc-map', pos.lat, pos.lng), 100);
}
} else {
showToast('格式错误,请使用:纬度,经度');
}
}
// ===== 刷新位置 =====
async function refreshLocation() {
const curEl = document.getElementById('loc-current');
if (curEl) curEl.textContent = '定位中...';
try {
const pos = await LocationModule.getCurrentPosition();
const sourceText = LocationModule.getSourceText(pos.source);
// 更新当前位置显示
if (curEl) {
const sourceText = LocationModule.getSourceText(pos.source);
if (pos.source === 'default') {
curEl.innerHTML = `${pos.lat.toFixed(6)}, ${pos.lng.toFixed(6)} <span style="color:#fa8c16;font-size:12px;">(${sourceText})</span>`;
curEl.innerHTML = `<span style="color:#999">${pos.lat.toFixed(4)}, ${pos.lng.toFixed(4)}</span>`;
} else if (pos.source === 'ip') {
curEl.innerHTML = `${pos.lat.toFixed(4)}, ${pos.lng.toFixed(4)} <span style="color:#1890ff;font-size:12px">(${sourceText})</span>`;
} else {
curEl.textContent = `${pos.lat.toFixed(6)}, ${pos.lng.toFixed(6)}`;
}
}
// 更新精度显示
const accEl = document.getElementById('loc-accuracy');
if (accEl) {
const sourceText = LocationModule.getSourceText(pos.source);
accEl.textContent = LocationModule.formatAccuracy(pos.accuracy) + ' · ' + sourceText;
let text = LocationModule.formatAccuracy(pos.accuracy) + ' · ' + sourceText;
if (pos.source === 'default') {
text = '❌ 定位失败 · 使用默认坐标';
} else if (pos.source === 'ip') {
text = '📡 ' + LocationModule.formatAccuracy(pos.accuracy) + ' · ' + sourceText + (pos.city ? ' (' + pos.city + ')' : '');
}
accEl.textContent = text;
}
// 更新上报时间
const reportEl = document.getElementById('loc-last-report');
if (reportEl) reportEl.textContent = new Date().toTimeString().split(' ')[0];
// 更新偏移距离
const offsetEl = document.getElementById('loc-offset');
if (offsetEl) {
offsetEl.textContent = '计算中...';
@ -512,24 +660,35 @@ const App = (() => {
}, 500);
}
// 初始化高德地图
// 初始化/更新地图
const mapContainer = document.getElementById('loc-map');
if (mapContainer && pos.source !== 'default') {
mapContainer.innerHTML = '';
mapContainer.style.display = 'block';
mapContainer.style.border = 'none';
setTimeout(() => {
LocationModule.initAmap('loc-map', pos.lat, pos.lng);
}, 100);
} else if (mapContainer && pos.source === 'default') {
mapContainer.innerHTML = '<div style="text-align:center;color:#999;padding:20px;">🚫 定位失败,无法加载地图<br><span style="font-size:12px">请检查GPS权限和定位服务</span></div>';
if (mapContainer) {
if (pos.source === 'default') {
// 定位完全失败,显示诊断信息
const reasons = await LocationModule.getLocationErrorReason();
mapContainer.innerHTML = `
<div style="text-align:center;color:#666;padding:15px;">
<div style="font-size:28px;margin-bottom:8px;">🚫</div>
<div style="font-size:14px;font-weight:bold;margin-bottom:8px;">定位失败</div>
<div style="font-size:12px;color:#999;text-align:left;display:inline-block;">
${reasons.map(r => '• ' + r).join('<br>')}
</div>
<div style="font-size:12px;color:#1890ff;margin-top:10px;">
💡 建议开启WiFi + GPS + 到窗边
</div>
</div>`;
} else {
mapContainer.innerHTML = '';
mapContainer.style.border = 'none';
setTimeout(() => LocationModule.showMap('loc-map', pos.lat, pos.lng), 100);
}
}
// 更新首页位置显示
updateHomeLocation();
} catch (e) {
if (curEl) curEl.textContent = '定位失败';
if (curEl) curEl.textContent = '定位异常';
console.error('refreshLocation错误:', e);
}
}
@ -601,10 +760,13 @@ const App = (() => {
submitDemand,
selectDropPoint,
confirmDropPoint,
searchDropPoint,
selectSearchResult,
updateDropPoint,
addAnnotate,
triggerSOS,
refreshLocation,
manualSetLocation,
showToast,
showServerConfig,
toggleSwitch,

@ -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
};
})();

Loading…
Cancel
Save