import baseConfig from '@/config/baseUrl.js'; const { TIANDITU_KEY } = baseConfig; import { requestAndroidPermission } from '@/uni_modules/x-perm-apply-instr/js_sdk/index.js'; class LocationService { /** * 初始化定位 (主入口) * 策略:优先 GPS -> 失败则 IP兜底 -> 统一逆地理编码 * @returns {Promise} 返回最终的定位数据 */ async initLocation(type = 'gps') { console.log('--- 开始初始化定位 ---'); try { let perId = 'android.permission.ACCESS_FINE_LOCATION', perm = uni.getStorageSync(`permision_${perId}`); // status 权限申请结果 (1:已获得权限, 0:拒绝本次申请, -1:永久拒绝申请) const perResult = [0, 1, -1].indexOf(perm) === -1 && type === 'gps' ? await requestAndroidPermission( perId, { title: '获取定位权限申请说明', content: '为了根据您的位置展示信息, 我们需要申请您设备的位置权限', }, true) : (perm || 0); // 通过权限结果走不通流程 let gpsResult; if (perResult === 1) { // 优先尝试 GPS 精准定位, 竞速机制避免超时 gpsResult = await Promise.race([this.getGPSLocation(), this.timePromise()]); } else { gpsResult = { status: 'fail' } } // 如果 GPS 定位失败(如用户拒绝权限或超时),则降级使用 IP 定位 if (gpsResult.status !== 'success') { console.log('GPS定位失败或被拒绝,降级使用 IP 定位'); await this.getIPLocation(); } // 等待逆地理编码完成(获取城市名),没有cityCode才去查 if (!uni.getStorageSync('cityCode')) { await this.updateGeocode(); } // 确保数据已更新到 Storage,返回最新数据 const finalLocation = uni.getStorageSync('positioning'); return finalLocation; } catch (error) { console.error('定位流程异常:', error); // 即使报错,也尝试返回缓存中的旧数据,避免页面崩溃 return uni.getStorageSync('positioning') || {}; } } // 获取 IP 定位 getIPLocation() { return new Promise(async (resolve) => { if (process.env.NODE_ENV === 'development') { let url = 'https://ip9.com.cn/get'; // #ifdef H5 // 正则匹配规则:覆盖 localhost、127.0.0.1、192.168.x.x、::1(IPv6回环) const localReg = /^(localhost|::1|127\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})|192\.168\.(\d{1,3})\.(\d{1,3}))$/; if (localReg.test(window.location.hostname.toLowerCase())) { url = '/getIp/get'; } // #endif uni.request({ url: url, method: 'GET', success: (res) => { console.log(res); if (res.data.ret == 200) { this.updateData({ latitude: res.data.data.lat, longitude: res.data.data.lng, source: 'ip', isRefuse: 1 }); } resolve(); }, fail: () => resolve() // 失败也 resolve,不阻断流程 }); } else { } }); } // 获取 GPS 原生定位 getGPSLocation() { return new Promise((resolve) => { uni.getLocation({ // #ifdef H5 type: 'wgs84', // #endif // #ifndef H5 type: 'gcj02', // #endif geocode: true, isHighAccuracy: true, success: async (res) => { console.log('GPS 定位成功', res); this.updateData({ latitude: res.latitude, longitude: res.longitude, source: 'gps', isRefuse: '' }); resolve({ status: 'success', data: res }); }, fail: (err) => { console.log('GPS 定位失败或拒绝', err); // 记录拒绝状态 this.updateData({ isRefuse: 1 }); resolve({ status: 'fail', error: err }); } }); }); } // 超时Promise timePromise(wait = 1000) { return new Promise((resolve) => { // 2秒超时阈值 setTimeout(() => { // 超时后返回「超时标识」,方便后续区分 resolve({ status: 'fail', error: 'GPS定位超时' }); }, wait); }) } // 更新storage updateData(data) { const current = uni.getStorageSync('positioning') || {}; uni.setStorageSync('positioning', { ...current, ...data }); } // 天地图逆地理编码 (经纬度 -> 城市信息),e为搜索时传值 updateGeocode(e) { return new Promise((resolve) => { let location = e || uni.getStorageSync('positioning'); if (!location || !location.longitude || !location.latitude) { return resolve(); } let postStr = { 'lon': location.longitude, 'lat': location.latitude, var: 1, }; uni.request({ url: `https://api.tianditu.gov.cn/geocoder?postStr=${encodeURIComponent(JSON.stringify(postStr))}&type=geocode&tk=${TIANDITU_KEY}`, success: (res) => { if (res.statusCode == 200 && res.data.status == 0) { let obj = res.data.result.addressComponent; // 经纬度所属城市,没有e说明是上面定位流程过来的 if (!e) this.updateData({ provinceCode: obj.province_code.slice(3), provinceName: obj.province, cityCode: obj.city_code.slice(3), cityName: obj.city, districtCode: obj.county_code.slice(3), districtName: obj.county, }); console.log('逆地理编码更新成功:', obj.city); } resolve(res); }, fail: () => resolve() }); }); } // 天地图地理编码 (城市信息 -> 经纬度) searchGeocode(e) { return new Promise((resolve) => { if (!e) { return resolve(); } let postStr = { 'keyWord': e }; uni.request({ url: `https://api.tianditu.gov.cn/geocoder?ds=${encodeURIComponent(JSON.stringify(postStr))}&tk=${TIANDITU_KEY}`, success: (res) => { if (res.statusCode == 200 && res.data.status == 0) { let obj = res.data.location; this.updateGeocode({ latitude: obj.lat, longitude: obj.lon, }); console.log('地理编码更新成功:', obj.keyWord); } resolve(); }, fail: () => resolve() }); }); } // 跳转到对应平台的系统设置页面 openSystemSettings() { console.log('--- 尝试打开系统设置,准备重新定位 ---'); // #ifdef APP-PLUS // App 端:直接调用 uni.openAppAuthorizeSetting uni.openAppAuthorizeSetting({ success(res) { console.log('App: 成功打开授权设置界面', res); }, fail(err) { console.error('App: 打开授权设置界面失败', err); }, complete: async () => { // 确保 App 重新获得焦点且系统状态已更新 await new Promise(r => setTimeout(r, 500)); // 重新执行完整的定位流程 console.log('App: 从系统设置返回,重新执行 initLocation'); await this.initLocation(); } }); // #endif // #ifdef MP // 小程序端:提示用户进入设置页。实际的小程序权限设置需要用户在页面内点击 open-type="openSetting" 按钮触发。 uni.showModal({ title: '授权提示', content: '请在小程序设置页中手动开启“位置信息”权限。', confirmText: '去设置', success: (res) => { if (res.confirm) { // 注意:uni.openSetting 在部分小程序平台可能已被废弃,且需要在用户交互后才能调用。 // 最佳实践是引导用户去一个包含 的页面。 uni.openSetting({ success: async (settingRes) => { console.log('MP: 用户完成设置', settingRes); if (settingRes.authSetting['scope.userLocation']) { // 权限开启,重新定位 await this.initLocation(); } }, fail: () => { console.log('MP: 打开设置页失败'); } }); } } }); // #endif // #ifdef H5 // H5 端:通常无法直接打开系统设置,只能提示用户手动操作 uni.showModal({ title: '授权提示', content: '请在浏览器或系统设置中手动开启定位权限。', showCancel: false, confirmText: '知道了' }); // #endif } } export default new LocationService();