Files
MER-2.2_2601/mer_uniapp/utils/locationManager.js
2026-03-08 20:07:52 +08:00

847 lines
24 KiB
JavaScript
Raw Permalink 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.
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
// #ifdef APP-PLUS
import permision from "./permission.js"
// #endif
/**
* 全局定位权限管理和数据缓存
*/
class LocationManager {
constructor() {
this.locationData = null;
this.hasLocationPermission = false;
this.permissionStatus = 0; // 0: 未检查, 1: 已允许, 2: 拒绝, 3: 系统未开启
this.isChecking = false;
this.listeners = [];
}
/**
* 初始化定位管理器
*/
init() {
// 从缓存中恢复定位数据
this.restoreLocationData();
// 检查权限状态
this.checkInitialPermission();
}
/**
* 从缓存中恢复定位数据
*/
restoreLocationData() {
try {
// 优先使用新的对象缓存格式
const cachedLocationInfo = uni.getStorageSync('location_info');
if (cachedLocationInfo && typeof cachedLocationInfo === 'object') {
this.locationData = {
latitude: parseFloat(cachedLocationInfo.latitude),
longitude: parseFloat(cachedLocationInfo.longitude),
name: cachedLocationInfo.name || '',
address: cachedLocationInfo.address || '',
updateTime: cachedLocationInfo.updateTime || 0,
source: cachedLocationInfo.source || '', // 恢复数据来源标识
type: cachedLocationInfo.type || '' // 恢复位置类型
};
this.hasLocationPermission = cachedLocationInfo.hasPermission || false;
this.permissionStatus = cachedLocationInfo.hasPermission ? 1 : 0;
return;
}
// 兼容旧版本的独立缓存项
const latitude = uni.getStorageSync('user_latitude');
const longitude = uni.getStorageSync('user_longitude');
const locationName = uni.getStorageSync('user_location_name');
const locationAddress = uni.getStorageSync('user_location_address');
const updateTime = uni.getStorageSync('location_update_time');
const hasPermission = uni.getStorageSync('has_location_permission');
if (latitude && longitude) {
this.locationData = {
latitude: parseFloat(latitude),
longitude: parseFloat(longitude),
name: locationName || '',
address: locationAddress || '',
updateTime: updateTime || 0
};
this.hasLocationPermission = !!hasPermission;
this.permissionStatus = hasPermission ? 1 : 0;
// 迁移到新格式并清除旧缓存
this.migrateToNewFormat();
}
} catch (error) {
}
}
/**
* 迁移旧格式缓存到新格式
*/
migrateToNewFormat() {
try {
// 保存到新格式
this.saveLocationDataToCache();
// 清除旧格式缓存
uni.removeStorageSync('user_latitude');
uni.removeStorageSync('user_longitude');
uni.removeStorageSync('user_location_name');
uni.removeStorageSync('user_location_address');
uni.removeStorageSync('location_update_time');
uni.removeStorageSync('has_location_permission');
} catch (error) {
}
}
/**
* 保存位置数据到缓存(使用对象格式)
*/
saveLocationDataToCache() {
try {
const locationInfo = {
latitude: this.locationData?.latitude || 0,
longitude: this.locationData?.longitude || 0,
name: this.locationData?.name || '',
address: this.locationData?.address || '',
updateTime: this.locationData?.updateTime || Date.now(),
hasPermission: this.hasLocationPermission,
permissionStatus: this.permissionStatus,
source: this.locationData?.source || '', // 保存数据来源标识
type: this.locationData?.type || '', // 保存位置类型
version: '2.0' // 版本标识
};
uni.setStorageSync('location_info', locationInfo);
} catch (error) {
}
}
/**
* 检查初始权限状态
*/
async checkInitialPermission() {
if (this.isChecking) return this.permissionStatus;
this.isChecking = true;
try {
let status = 0;
// #ifdef APP-PLUS
status = permision.isIOS ?
await permision.requestIOS('location') :
await permision.requestAndroid('android.permission.ACCESS_FINE_LOCATION');
// #endif
// #ifdef MP
status = await this.getSetting();
// #endif
// #ifdef H5
// H5环境下通过尝试获取定位来判断权限
try {
await this.testLocationAccess();
status = 1;
} catch (error) {
status = 2;
}
// #endif
this.permissionStatus = status;
this.hasLocationPermission = status === 1;
} catch (error) {
this.permissionStatus = 2;
this.hasLocationPermission = false;
} finally {
this.isChecking = false;
}
return this.permissionStatus;
}
/**
* 测试定位访问权限H5环境
*/
testLocationAccess() {
return new Promise((resolve, reject) => {
// #ifdef H5
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => resolve(position),
(error) => {
reject(error);
},
{ timeout: 10000, enableHighAccuracy: false, maximumAge: 60000 }
);
} else {
reject(new Error('Geolocation not supported'));
}
// #endif
// #ifndef H5
// 非H5环境直接resolve由其他逻辑处理
resolve(true);
// #endif
});
}
/**
* 获取小程序定位权限设置
*/
getSetting() {
return new Promise((resolve) => {
// #ifdef MP
uni.getSetting({
success: (res) => {
if (res.authSetting['scope.userLocation'] === undefined) {
resolve(0); // 未询问
} else if (res.authSetting['scope.userLocation']) {
resolve(1); // 已允许
} else {
resolve(2); // 已拒绝
}
},
fail: () => resolve(2)
});
// #endif
// #ifndef MP
resolve(0);
// #endif
});
}
/**
* 实时检查系统权限状态(不依赖缓存)
*/
async checkRealTimePermission() {
try {
let status = 0;
// #ifdef APP-PLUS
status = permision.isIOS ?
await permision.requestIOS('location') :
await permision.requestAndroid('android.permission.ACCESS_FINE_LOCATION');
// #endif
// #ifdef MP
status = await this.getSetting();
// #endif
// #ifdef H5
// H5环境下通过实际尝试获取定位来检查权限
try {
await this.testLocationAccess();
status = 1;
} catch (error) {
status = 2;
}
// #endif
return status;
} catch (error) {
return 2; // 检查失败视为无权限
}
}
/**
* 请求定位权限并获取位置信息
*/
async requestLocationPermission(showTip = true) {
if (this.isChecking) return false;
try {
const permissionStatus = await this.checkInitialPermission();
if (permissionStatus === 1) {
// 权限已获得,尝试获取定位
try {
const locationData = await this.getCurrentLocation();
if (locationData) {
await this.saveLocationData(locationData);
this.notifyListeners('permission_granted', locationData);
return true;
}
} catch (locationError) {
if (showTip) {
this.showLocationErrorDialog(locationError.userMessage);
}
return false;
}
} else if (permissionStatus === 2) {
// 权限被拒绝
if (showTip) {
this.showLocationPermissionDeniedDialog();
}
this.notifyListeners('permission_denied');
return false;
} else if (permissionStatus === 0) {
// 未询问权限,尝试获取定位
try {
const locationData = await this.getCurrentLocation();
if (locationData) {
await this.saveLocationData(locationData);
this.permissionStatus = 1;
this.hasLocationPermission = true;
this.notifyListeners('permission_granted', locationData);
return true;
}
} catch (locationError) {
// 首次获取失败可能是用户拒绝,更新状态
this.permissionStatus = 2;
this.hasLocationPermission = false;
if (showTip) {
this.showLocationPermissionDeniedDialog();
}
return false;
}
}
} catch (error) {
if (showTip) {
this.showLocationErrorDialog();
}
this.notifyListeners('permission_error', error);
}
return false;
}
/**
* 获取当前位置信息
*/
getCurrentLocation() {
return new Promise((resolve, reject) => {
// 小程序定位参数优化
const locationOptions = {
// #ifdef MP
type: 'wgs84', // 小程序推荐使用wgs84兼容性更好
isHighAccuracy: false, // 降低精度要求,提高成功率
timeout: 15000, // 设置超时时间15秒
// #endif
// #ifdef H5 || APP-PLUS
type: 'gcj02',
isHighAccuracy: true,
timeout: 10000,
// #endif
};
// 开始获取位置
uni.getLocation({
...locationOptions,
success: (res) => {
const locationData = {
latitude: res.latitude,
longitude: res.longitude,
accuracy: res.accuracy,
altitude: res.altitude,
speed: res.speed,
updateTime: Date.now()
};
resolve(locationData);
},
fail: (error) => {
// 获取定位失败
// 根据具体错误提供更详细的信息
let errorMessage = '获取位置失败';
if (error.errMsg) {
if (error.errMsg.includes('timeout')) {
errorMessage = '定位超时请检查网络连接和GPS设置';
} else if (error.errMsg.includes('system permission denied')) {
errorMessage = '系统定位权限被拒绝,请在系统设置中开启定位服务';
} else if (error.errMsg.includes('location service unavailable')) {
errorMessage = '定位服务不可用请确保GPS已开启';
} else if (error.errMsg.includes('network')) {
errorMessage = '网络定位失败,请检查网络连接';
}
}
error.userMessage = errorMessage;
reject(error);
}
});
});
}
/**
* 保存定位数据到缓存
* @param {Object} locationData 位置数据
* @param {String} locationName 位置名称
* @param {String} locationAddress 位置地址
* @param {Boolean} skipPermissionCheck 是否跳过权限检查(用于地图选择场景)
*/
async saveLocationData(locationData, locationName = '', locationAddress = '', skipPermissionCheck = false) {
let canSave = false;
if (skipPermissionCheck) {
// 地图选择场景,跳过权限检查
canSave = true;
} else {
// GPS定位场景需要检查权限
const realTimePermissionStatus = await this.checkRealTimePermission();
canSave = realTimePermissionStatus === 1;
}
if (canSave) {
this.locationData = {
...locationData,
name: locationName,
address: locationAddress,
source: skipPermissionCheck ? 'manual_select' : 'gps', // 根据场景设置数据来源
type: skipPermissionCheck ? 'business' : 'gps' // 根据场景设置位置类型
};
// 设置位置选择成功标识
this.hasLocationPermission = true;
this.permissionStatus = 1;
// 使用新的对象格式保存到缓存
this.saveLocationDataToCache();
} else {
// 清除可能过期的权限标识和位置数据
this.hasLocationPermission = false;
this.permissionStatus = 2;
this.clearLocationData();
}
}
/**
* 清除定位数据
*/
clearLocationData() {
this.locationData = null;
this.hasLocationPermission = false;
this.permissionStatus = 2;
// 清除新格式缓存
uni.removeStorageSync('location_info');
// 兼容性:清除可能存在的旧格式缓存
uni.removeStorageSync('user_latitude');
uni.removeStorageSync('user_longitude');
uni.removeStorageSync('user_location_name');
uni.removeStorageSync('user_location_address');
uni.removeStorageSync('location_update_time');
uni.removeStorageSync('has_location_permission');
}
/**
* 清理历史圈层缓存(小程序首次启动时使用)
*/
clearHistoricalAreaCache() {
try {
// 清除areas_info缓存
uni.removeStorageSync('areas_info');
} catch (error) {
console.warn('清理历史圈层缓存失败:', error);
}
}
/**
* 检查是否有定位权限(实时检查)
*/
async hasPermission() {
// 检查是否已有用户选择的地址,如果有就不要覆盖
const cachedLocationInfo = uni.getStorageSync('location_info');
if (cachedLocationInfo && (cachedLocationInfo.source === 'manual_select' || cachedLocationInfo.type === 'business')) {
// 直接返回权限状态不触发GPS定位
this.permissionStatus = 1;
this.hasLocationPermission = true;
return true;
}
// 进行实时权限检查而不是依赖缓存
const realTimeStatus = await this.checkRealTimePermission();
// 更新内部状态
this.permissionStatus = realTimeStatus;
this.hasLocationPermission = realTimeStatus === 1;
// 如果权限被撤销,清除相关缓存
if (realTimeStatus !== 1) {
this.clearLocationData();
} else {
// 权限有效,更新缓存标识(但不覆盖位置数据)
if (!cachedLocationInfo) {
this.saveLocationDataToCache();
}
}
return this.hasLocationPermission;
}
/**
* 获取缓存的定位数据(实时从缓存读取)
*/
getLocationData() {
// 实时从缓存读取最新数据,而不是返回内存中的旧数据
const cachedLocationInfo = uni.getStorageSync('location_info');
if (cachedLocationInfo && typeof cachedLocationInfo === 'object') {
return {
latitude: parseFloat(cachedLocationInfo.latitude),
longitude: parseFloat(cachedLocationInfo.longitude),
name: cachedLocationInfo.name || '',
address: cachedLocationInfo.address || '',
updateTime: cachedLocationInfo.updateTime || 0,
source: cachedLocationInfo.source || '',
type: cachedLocationInfo.type || ''
};
}
// 如果没有缓存,返回内存中的数据(兼容旧逻辑)
return this.locationData;
}
/**
* 获取完整的位置信息(包括缓存状态)
*/
getFullLocationInfo() {
const cachedLocationInfo = uni.getStorageSync('location_info');
return {
locationData: this.locationData,
hasPermission: this.hasLocationPermission,
permissionStatus: this.permissionStatus,
isExpired: this.isLocationDataExpired(),
cachedInfo: cachedLocationInfo,
cacheExists: !!cachedLocationInfo
};
}
/**
* 检查缓存是否存在且有效
*/
isCacheValid() {
const cachedLocationInfo = uni.getStorageSync('location_info');
if (!cachedLocationInfo || typeof cachedLocationInfo !== 'object') {
return false;
}
// 检查必要字段
if (!cachedLocationInfo.latitude || !cachedLocationInfo.longitude) {
return false;
}
// 检查是否过期
if (this.isLocationDataExpired()) {
return false;
}
return true;
}
/**
* 判断定位数据是否过期默认30分钟
*/
isLocationDataExpired(maxAge = 30 * 60 * 1000) {
if (!this.locationData || !this.locationData.updateTime) {
return true;
}
return (Date.now() - this.locationData.updateTime) > maxAge;
}
/**
* 显示定位权限被拒绝的对话框
*/
showLocationPermissionDeniedDialog() {
// #ifdef MP
// 小程序:提供详细的权限开启步骤
uni.showModal({
title: '需要位置权限',
content: '为了为您推荐附近商圈,需要获取位置权限。\n\n您可以\n• 点击"去设置"在小程序设置中开启\n• 或重新进入页面时选择"允许"',
showCancel: true,
cancelText: '稍后再说',
confirmText: '去设置',
confirmColor: '#f55850',
success: (res) => {
if (res.confirm) {
this.openLocationSetting();
}
}
});
// #endif
// #ifdef H5
// H5浏览器权限引导
uni.showModal({
title: '需要位置权限',
content: '为了为您推荐附近商圈,需要获取位置权限。\n\n请在浏览器中\n• 点击地址栏左侧的位置图标\n• 选择"始终允许"\n• 刷新页面重试',
showCancel: true,
cancelText: '知道了',
confirmText: '我已设置',
confirmColor: '#f55850',
success: (res) => {
if (res.confirm) {
// H5 中重新尝试获取权限
location.reload();
}
}
});
// #endif
// #ifdef APP-PLUS
// APP系统权限引导
uni.showModal({
title: '需要位置权限',
content: '为了为您推荐附近商圈,需要获取位置权限。\n\n请在系统设置中\n• 找到本应用\n• 开启"位置信息"权限\n• 返回应用重试',
showCancel: true,
cancelText: '稍后再说',
confirmText: '去设置',
confirmColor: '#f55850',
success: (res) => {
if (res.confirm) {
this.openLocationSetting();
}
}
});
// #endif
}
/**
* 显示定位错误对话框
*/
showLocationErrorDialog(customMessage) {
if (customMessage) {
// 使用自定义错误消息
uni.showModal({
title: '定位失败',
content: customMessage,
showCancel: true,
cancelText: '稍后再试',
confirmText: '重新定位',
confirmColor: '#f55850',
success: (res) => {
if (res.confirm) {
this.requestLocationPermission(false);
}
}
});
return;
}
// #ifdef MP
// 小程序:针对性的问题排查
uni.showModal({
title: '定位失败',
content: '获取位置信息失败,请检查:\n\n• 手机GPS定位是否开启\n• 网络连接是否正常\n• 微信位置权限是否已授权\n\n建议重新进入页面再试',
showCancel: true,
cancelText: '稍后再试',
confirmText: '重新定位',
confirmColor: '#f55850',
success: (res) => {
if (res.confirm) {
this.requestLocationPermission(false);
}
}
});
// #endif
// #ifdef H5
// H5浏览器特有问题
uni.showModal({
title: '定位失败',
content: '获取位置信息失败,请检查:\n\n• 浏览器位置权限是否已开启\n• 网络连接是否正常\n• 是否允许HTTPS访问位置\n\n建议刷新页面重试',
showCancel: true,
cancelText: '稍后再试',
confirmText: '刷新重试',
confirmColor: '#f55850',
success: (res) => {
if (res.confirm) {
location.reload();
}
}
});
// #endif
// #ifdef APP-PLUS
// APP系统级问题
uni.showModal({
title: '定位失败',
content: '获取位置信息失败,请检查:\n\n• 系统位置服务是否开启\n• 应用位置权限是否已授权\n• 网络连接是否正常\n\n建议检查系统设置后重试',
showCancel: true,
cancelText: '稍后再试',
confirmText: '重新定位',
confirmColor: '#f55850',
success: (res) => {
if (res.confirm) {
this.requestLocationPermission(false);
}
}
});
// #endif
}
/**
* 打开定位设置
*/
openLocationSetting() {
// #ifdef APP-PLUS
if (permision) {
permision.gotoAppSetting();
}
// #endif
// #ifdef MP
uni.openSetting({
success: (res) => {
if (res.authSetting && res.authSetting['scope.userLocation']) {
this.requestLocationPermission(false);
}
}
});
// #endif
// #ifdef H5
uni.showToast({
title: '请在浏览器设置中允许位置访问',
icon: 'none',
duration: 3000
});
// #endif
}
/**
* 决定跳转路径
*/
getLocationPagePath() {
if (this.hasLocationPermission && this.locationData) {
return '/pages/circle/select';
} else {
return '/pages/circle/index';
}
}
/**
* 智能跳转到定位页面
*/
navigateToLocationPage(params = {}) {
const path = this.getLocationPagePath();
const url = Object.keys(params).length > 0 ?
`${path}?${Object.keys(params).map(key => `${key}=${encodeURIComponent(params[key])}`).join('&')}` :
path;
uni.navigateTo({
url: url,
fail: () => {
// 如果跳转失败,尝试重定向
uni.redirectTo({ url: url });
}
});
}
/**
* 添加监听器
*/
addListener(callback) {
this.listeners.push(callback);
}
/**
* 移除监听器
*/
removeListener(callback) {
const index = this.listeners.indexOf(callback);
if (index > -1) {
this.listeners.splice(index, 1);
}
}
/**
* 通知所有监听器
*/
notifyListeners(event, data) {
this.listeners.forEach(callback => {
try {
callback(event, data);
} catch (error) {
}
});
}
/**
* 刷新定位数据 - 改为使用地图选择
*/
async refreshLocationData(showLoading = true) {
return new Promise((resolve, reject) => {
// 使用当前位置作为地图中心点(如果有的话)
const chooseLocationOptions = {};
if (this.locationData) {
chooseLocationOptions.latitude = this.locationData.latitude;
chooseLocationOptions.longitude = this.locationData.longitude;
}
// 直接打开系统地图选择位置
uni.chooseLocation({
...chooseLocationOptions,
success: async (res) => {
try {
// 保存用户选择的位置(跳过权限检查)
await this.saveLocationData({
latitude: res.latitude,
longitude: res.longitude,
updateTime: Date.now()
}, res.name, res.address, true);
if (showLoading) {
uni.showToast({
title: '位置已更新',
icon: 'success'
});
}
resolve(this.locationData);
} catch (error) {
if (showLoading) {
uni.showToast({
title: '保存位置失败',
icon: 'none'
});
}
reject(error);
}
},
fail: (error) => {
// 如果不是用户取消,显示错误提示
if (showLoading && error.errMsg && !error.errMsg.includes('cancel')) {
uni.showToast({
title: '位置选择失败',
icon: 'none'
});
}
// 用户取消也算是一种处理结果返回null而不是reject
resolve(null);
}
});
});
}
}
// 创建全局单例
const locationManager = new LocationManager();
export default locationManager;