| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847 |
- // +----------------------------------------------------------------------
- // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
- // +----------------------------------------------------------------------
- // | Copyright (c) 2016~2025 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;
|