location.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import baseConfig from '@/config/baseUrl.js';
  2. const {
  3. TIANDITU_KEY
  4. } = baseConfig;
  5. import {
  6. requestAndroidPermission
  7. } from '@/uni_modules/x-perm-apply-instr/js_sdk/index.js';
  8. class LocationService {
  9. /**
  10. * 初始化定位 (主入口)
  11. * 策略:优先 GPS -> 失败则 IP兜底 -> 统一逆地理编码
  12. * @returns {Promise<Object>} 返回最终的定位数据
  13. */
  14. async initLocation(type = 'gps') {
  15. console.log('--- 开始初始化定位 ---');
  16. try {
  17. let perId = 'android.permission.ACCESS_FINE_LOCATION',
  18. perm = uni.getStorageSync(`permision_${perId}`);
  19. // status 权限申请结果 (1:已获得权限, 0:拒绝本次申请, -1:永久拒绝申请)
  20. const perResult = [0, 1, -1].indexOf(perm) === -1 && type === 'gps' ? await requestAndroidPermission(
  21. perId, {
  22. title: '获取定位权限申请说明',
  23. content: '为了根据您的位置展示信息, 我们需要申请您设备的位置权限',
  24. }, true) : (perm || 0);
  25. // 通过权限结果走不通流程
  26. let gpsResult;
  27. if (perResult === 1) {
  28. // 优先尝试 GPS 精准定位, 竞速机制避免超时
  29. gpsResult = await Promise.race([this.getGPSLocation(), this.timePromise()]);
  30. } else {
  31. gpsResult = {
  32. status: 'fail'
  33. }
  34. }
  35. // 如果 GPS 定位失败(如用户拒绝权限或超时),则降级使用 IP 定位
  36. if (gpsResult.status !== 'success') {
  37. console.log('GPS定位失败或被拒绝,降级使用 IP 定位');
  38. await this.getIPLocation();
  39. }
  40. // 等待逆地理编码完成(获取城市名),没有cityCode才去查
  41. if (!uni.getStorageSync('cityCode')) {
  42. await this.updateGeocode();
  43. }
  44. // 确保数据已更新到 Storage,返回最新数据
  45. const finalLocation = uni.getStorageSync('positioning');
  46. return finalLocation;
  47. } catch (error) {
  48. console.error('定位流程异常:', error);
  49. // 即使报错,也尝试返回缓存中的旧数据,避免页面崩溃
  50. return uni.getStorageSync('positioning') || {};
  51. }
  52. }
  53. // 获取 IP 定位
  54. getIPLocation() {
  55. return new Promise(async (resolve) => {
  56. if (process.env.NODE_ENV === 'development') {
  57. let url = 'https://ip9.com.cn/get';
  58. // #ifdef H5
  59. // 正则匹配规则:覆盖 localhost、127.0.0.1、192.168.x.x、::1(IPv6回环)
  60. const localReg =
  61. /^(localhost|::1|127\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})|192\.168\.(\d{1,3})\.(\d{1,3}))$/;
  62. if (localReg.test(window.location.hostname.toLowerCase())) {
  63. url = '/getIp/get';
  64. }
  65. // #endif
  66. uni.request({
  67. url: url,
  68. method: 'GET',
  69. success: (res) => {
  70. console.log(res);
  71. if (res.data.ret == 200) {
  72. this.updateData({
  73. latitude: res.data.data.lat,
  74. longitude: res.data.data.lng,
  75. source: 'ip',
  76. isRefuse: 1
  77. });
  78. }
  79. resolve();
  80. },
  81. fail: () => resolve() // 失败也 resolve,不阻断流程
  82. });
  83. } else {
  84. }
  85. });
  86. }
  87. // 获取 GPS 原生定位
  88. getGPSLocation() {
  89. return new Promise((resolve) => {
  90. uni.getLocation({
  91. // #ifdef H5
  92. type: 'wgs84',
  93. // #endif
  94. // #ifndef H5
  95. type: 'gcj02',
  96. // #endif
  97. geocode: true,
  98. isHighAccuracy: true,
  99. success: async (res) => {
  100. console.log('GPS 定位成功', res);
  101. this.updateData({
  102. latitude: res.latitude,
  103. longitude: res.longitude,
  104. source: 'gps',
  105. isRefuse: ''
  106. });
  107. resolve({
  108. status: 'success',
  109. data: res
  110. });
  111. },
  112. fail: (err) => {
  113. console.log('GPS 定位失败或拒绝', err);
  114. // 记录拒绝状态
  115. this.updateData({
  116. isRefuse: 1
  117. });
  118. resolve({
  119. status: 'fail',
  120. error: err
  121. });
  122. }
  123. });
  124. });
  125. }
  126. // 超时Promise
  127. timePromise(wait = 1000) {
  128. return new Promise((resolve) => {
  129. // 2秒超时阈值
  130. setTimeout(() => {
  131. // 超时后返回「超时标识」,方便后续区分
  132. resolve({
  133. status: 'fail',
  134. error: 'GPS定位超时'
  135. });
  136. }, wait);
  137. })
  138. }
  139. // 更新storage
  140. updateData(data) {
  141. const current = uni.getStorageSync('positioning') || {};
  142. uni.setStorageSync('positioning', {
  143. ...current,
  144. ...data
  145. });
  146. }
  147. // 天地图逆地理编码 (经纬度 -> 城市信息),e为搜索时传值
  148. updateGeocode(e) {
  149. return new Promise((resolve) => {
  150. let location = e || uni.getStorageSync('positioning');
  151. if (!location || !location.longitude || !location.latitude) {
  152. return resolve();
  153. }
  154. let postStr = {
  155. 'lon': location.longitude,
  156. 'lat': location.latitude,
  157. var: 1,
  158. };
  159. uni.request({
  160. url: `https://api.tianditu.gov.cn/geocoder?postStr=${encodeURIComponent(JSON.stringify(postStr))}&type=geocode&tk=${TIANDITU_KEY}`,
  161. success: (res) => {
  162. if (res.statusCode == 200 && res.data.status == 0) {
  163. let obj = res.data.result.addressComponent;
  164. // 经纬度所属城市,没有e说明是上面定位流程过来的
  165. if (!e) this.updateData({
  166. provinceCode: obj.province_code.slice(3),
  167. provinceName: obj.province,
  168. cityCode: obj.city_code.slice(3),
  169. cityName: obj.city,
  170. districtCode: obj.county_code.slice(3),
  171. districtName: obj.county,
  172. });
  173. console.log('逆地理编码更新成功:', obj.city);
  174. }
  175. resolve(res);
  176. },
  177. fail: () => resolve()
  178. });
  179. });
  180. }
  181. // 天地图地理编码 (城市信息 -> 经纬度)
  182. searchGeocode(e) {
  183. return new Promise((resolve) => {
  184. if (!e) {
  185. return resolve();
  186. }
  187. let postStr = {
  188. 'keyWord': e
  189. };
  190. uni.request({
  191. url: `https://api.tianditu.gov.cn/geocoder?ds=${encodeURIComponent(JSON.stringify(postStr))}&tk=${TIANDITU_KEY}`,
  192. success: (res) => {
  193. if (res.statusCode == 200 && res.data.status == 0) {
  194. let obj = res.data.location;
  195. this.updateGeocode({
  196. latitude: obj.lat,
  197. longitude: obj.lon,
  198. });
  199. console.log('地理编码更新成功:', obj.keyWord);
  200. }
  201. resolve();
  202. },
  203. fail: () => resolve()
  204. });
  205. });
  206. }
  207. // 跳转到对应平台的系统设置页面
  208. openSystemSettings() {
  209. console.log('--- 尝试打开系统设置,准备重新定位 ---');
  210. // #ifdef APP-PLUS
  211. // App 端:直接调用 uni.openAppAuthorizeSetting
  212. uni.openAppAuthorizeSetting({
  213. success(res) {
  214. console.log('App: 成功打开授权设置界面', res);
  215. },
  216. fail(err) {
  217. console.error('App: 打开授权设置界面失败', err);
  218. },
  219. complete: async () => {
  220. // 确保 App 重新获得焦点且系统状态已更新
  221. await new Promise(r => setTimeout(r, 500));
  222. // 重新执行完整的定位流程
  223. console.log('App: 从系统设置返回,重新执行 initLocation');
  224. await this.initLocation();
  225. }
  226. });
  227. // #endif
  228. // #ifdef MP
  229. // 小程序端:提示用户进入设置页。实际的小程序权限设置需要用户在页面内点击 open-type="openSetting" 按钮触发。
  230. uni.showModal({
  231. title: '授权提示',
  232. content: '请在小程序设置页中手动开启“位置信息”权限。',
  233. confirmText: '去设置',
  234. success: (res) => {
  235. if (res.confirm) {
  236. // 注意:uni.openSetting 在部分小程序平台可能已被废弃,且需要在用户交互后才能调用。
  237. // 最佳实践是引导用户去一个包含 <button open-type="openSetting"></button> 的页面。
  238. uni.openSetting({
  239. success: async (settingRes) => {
  240. console.log('MP: 用户完成设置', settingRes);
  241. if (settingRes.authSetting['scope.userLocation']) {
  242. // 权限开启,重新定位
  243. await this.initLocation();
  244. }
  245. },
  246. fail: () => {
  247. console.log('MP: 打开设置页失败');
  248. }
  249. });
  250. }
  251. }
  252. });
  253. // #endif
  254. // #ifdef H5
  255. // H5 端:通常无法直接打开系统设置,只能提示用户手动操作
  256. uni.showModal({
  257. title: '授权提示',
  258. content: '请在浏览器或系统设置中手动开启定位权限。',
  259. showCancel: false,
  260. confirmText: '知道了'
  261. });
  262. // #endif
  263. }
  264. }
  265. export default new LocationService();