// 地图工具函数 - 内联腾讯地图SDK核心代码 const DEFAULT_LOCALE = 'zh-CN' const permissionTexts = { 'zh-CN': { locationTitle: '位置信息授权', locationNeed: '需要获取您的位置信息以展示附近设备,请在“设置-权限管理”中开启定位权限。', locationDenied: '未授权定位,将无法展示附近设备。您可以稍后在“设置-权限管理”中重新开启定位权限。', goToSettings: '去设置', later: '暂不', gotIt: '知道了' }, 'en-US': { locationTitle: 'Location Permission', locationNeed: 'We need your location to show nearby devices. Please enable location in “Settings > Permissions”.', locationDenied: 'Location access is disabled. You can re-enable it later in “Settings > Permissions”.', goToSettings: 'Set', later: 'Skip', gotIt: 'OK' } } const getCurrentLocale = () => { try { const saved = uni.getStorageSync('language') if (saved && permissionTexts[saved]) { return saved } } catch (_) {} return DEFAULT_LOCALE } const getPermissionText = (key) => { const locale = getCurrentLocale() const texts = permissionTexts[locale] || permissionTexts[DEFAULT_LOCALE] return texts[key] || permissionTexts[DEFAULT_LOCALE][key] || '' } // 腾讯地图Key const QQMAP_KEY = 'RO5BZ-ECZ63-7US3C-RT5QW-TIDZE-2FF35'; // 高德地图配置(支付宝小程序专用) const AMAP_KEY = 'a07af802ff0a04f012954ff4e69b36d0'; const AMAP_SECURITY_KEY = '119f36535ab42b8b2c857f29f9320b06'; const AMAP_BASE_URL = 'https://restapi.amap.com/v3'; const AMAP_PATH_PREFIX = '/v3'; // 运行环境标识(编译期指令) const isAlipayEnv = (() => { let flag = false; // #ifdef MP-ALIPAY flag = true; // #endif return flag; })(); // 轻量级 MD5 实现(用于生成高德安全 sig) function md5(string) { function RotateLeft(lValue, iShiftBits) { return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits)); } function AddUnsigned(lX, lY) { const lX4 = lX & 0x40000000; const lY4 = lY & 0x40000000; const lX8 = lX & 0x80000000; const lY8 = lY & 0x80000000; const lResult = (lX & 0x3fffffff) + (lY & 0x3fffffff); if (lX4 & lY4) { return lResult ^ 0x80000000 ^ lX8 ^ lY8; } if (lX4 | lY4) { if (lResult & 0x40000000) { return lResult ^ 0xc0000000 ^ lX8 ^ lY8; } return lResult ^ 0x40000000 ^ lX8 ^ lY8; } return lResult ^ lX8 ^ lY8; } function F(x, y, z) { return (x & y) | (~x & z); } function G(x, y, z) { return (x & z) | (y & ~z); } function H(x, y, z) { return x ^ y ^ z; } function I(x, y, z) { return y ^ (x | ~z); } function FF(a, b, c, d, x, s, ac) { a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac)); return AddUnsigned(RotateLeft(a, s), b); } function GG(a, b, c, d, x, s, ac) { a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac)); return AddUnsigned(RotateLeft(a, s), b); } function HH(a, b, c, d, x, s, ac) { a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac)); return AddUnsigned(RotateLeft(a, s), b); } function II(a, b, c, d, x, s, ac) { a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac)); return AddUnsigned(RotateLeft(a, s), b); } function ConvertToWordArray(str) { const lWordArray = []; let lMessageLength = str.length; let lNumberOfWordsTempOne = lMessageLength + 8; const lNumberOfWordsTempTwo = (lNumberOfWordsTempOne - (lNumberOfWordsTempOne % 64)) / 64; const lNumberOfWords = (lNumberOfWordsTempTwo + 1) * 16; let lBytePosition = 0; let lByteCount = 0; while (lByteCount < lMessageLength) { const lWordCount = (lByteCount - (lByteCount % 4)) / 4; lBytePosition = (lByteCount % 4) * 8; lWordArray[lWordCount] = (lWordArray[lWordCount] || 0) | (str.charCodeAt(lByteCount) << lBytePosition); lByteCount++; } const lWordCount = (lByteCount - (lByteCount % 4)) / 4; lBytePosition = (lByteCount % 4) * 8; lWordArray[lWordCount] = lWordArray[lWordCount] || 0; lWordArray[lWordCount] |= 0x80 << lBytePosition; lWordArray[lNumberOfWords - 2] = lMessageLength << 3; lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29; return lWordArray; } function WordToHex(lValue) { let WordToHexValue = ''; for (let lCount = 0; lCount <= 3; lCount++) { const lByte = (lValue >>> (lCount * 8)) & 255; const WordToHexValueTemp = '0' + lByte.toString(16); WordToHexValue += WordToHexValueTemp.substr(WordToHexValueTemp.length - 2, 2); } return WordToHexValue; } function Utf8Encode(str) { str = str.replace(/\r\n/g, '\n'); let utftext = ''; for (let n = 0; n < str.length; n++) { const c = str.charCodeAt(n); if (c < 128) { utftext += String.fromCharCode(c); } else if (c > 127 && c < 2048) { utftext += String.fromCharCode((c >> 6) | 192); utftext += String.fromCharCode((c & 63) | 128); } else { utftext += String.fromCharCode((c >> 12) | 224); utftext += String.fromCharCode(((c >> 6) & 63) | 128); utftext += String.fromCharCode((c & 63) | 128); } } return utftext; } let x = []; let k, AA, BB, CC, DD, a, b, c, d; const S11 = 7, S12 = 12, S13 = 17, S14 = 22; const S21 = 5, S22 = 9, S23 = 14, S24 = 20; const S31 = 4, S32 = 11, S33 = 16, S34 = 23; const S41 = 6, S42 = 10, S43 = 15, S44 = 21; string = Utf8Encode(String(string)); x = ConvertToWordArray(string); a = 0x67452301; b = 0xefcdab89; c = 0x98badcfe; d = 0x10325476; for (k = 0; k < x.length; k += 16) { AA = a; BB = b; CC = c; DD = d; a = FF(a, b, c, d, x[k + 0], S11, 0xd76aa478); d = FF(d, a, b, c, x[k + 1], S12, 0xe8c7b756); c = FF(c, d, a, b, x[k + 2], S13, 0x242070db); b = FF(b, c, d, a, x[k + 3], S14, 0xc1bdceee); a = FF(a, b, c, d, x[k + 4], S11, 0xf57c0faf); d = FF(d, a, b, c, x[k + 5], S12, 0x4787c62a); c = FF(c, d, a, b, x[k + 6], S13, 0xa8304613); b = FF(b, c, d, a, x[k + 7], S14, 0xfd469501); a = FF(a, b, c, d, x[k + 8], S11, 0x698098d8); d = FF(d, a, b, c, x[k + 9], S12, 0x8b44f7af); c = FF(c, d, a, b, x[k + 10], S13, 0xffff5bb1); b = FF(b, c, d, a, x[k + 11], S14, 0x895cd7be); a = FF(a, b, c, d, x[k + 12], S11, 0x6b901122); d = FF(d, a, b, c, x[k + 13], S12, 0xfd987193); c = FF(c, d, a, b, x[k + 14], S13, 0xa679438e); b = FF(b, c, d, a, x[k + 15], S14, 0x49b40821); a = GG(a, b, c, d, x[k + 1], S21, 0xf61e2562); d = GG(d, a, b, c, x[k + 6], S22, 0xc040b340); c = GG(c, d, a, b, x[k + 11], S23, 0x265e5a51); b = GG(b, c, d, a, x[k + 0], S24, 0xe9b6c7aa); a = GG(a, b, c, d, x[k + 5], S21, 0xd62f105d); d = GG(d, a, b, c, x[k + 10], S22, 0x2441453); c = GG(c, d, a, b, x[k + 15], S23, 0xd8a1e681); b = GG(b, c, d, a, x[k + 4], S24, 0xe7d3fbc8); a = GG(a, b, c, d, x[k + 9], S21, 0x21e1cde6); d = GG(d, a, b, c, x[k + 14], S22, 0xc33707d6); c = GG(c, d, a, b, x[k + 3], S23, 0xf4d50d87); b = GG(b, c, d, a, x[k + 8], S24, 0x455a14ed); a = GG(a, b, c, d, x[k + 13], S21, 0xa9e3e905); d = GG(d, a, b, c, x[k + 2], S22, 0xfcefa3f8); c = GG(c, d, a, b, x[k + 7], S23, 0x676f02d9); b = GG(b, c, d, a, x[k + 12], S24, 0x8d2a4c8a); a = HH(a, b, c, d, x[k + 5], S31, 0xfffa3942); d = HH(d, a, b, c, x[k + 8], S32, 0x8771f681); c = HH(c, d, a, b, x[k + 11], S33, 0x6d9d6122); b = HH(b, c, d, a, x[k + 14], S34, 0xfde5380c); a = HH(a, b, c, d, x[k + 1], S31, 0xa4beea44); d = HH(d, a, b, c, x[k + 4], S32, 0x4bdecfa9); c = HH(c, d, a, b, x[k + 7], S33, 0xf6bb4b60); b = HH(b, c, d, a, x[k + 10], S34, 0xbebfbc70); a = HH(a, b, c, d, x[k + 13], S31, 0x289b7ec6); d = HH(d, a, b, c, x[k + 0], S32, 0xeaa127fa); c = HH(c, d, a, b, x[k + 3], S33, 0xd4ef3085); b = HH(b, c, d, a, x[k + 6], S34, 0x4881d05); a = HH(a, b, c, d, x[k + 9], S31, 0xd9d4d039); d = HH(d, a, b, c, x[k + 12], S32, 0xe6db99e5); c = HH(c, d, a, b, x[k + 15], S33, 0x1fa27cf8); b = HH(b, c, d, a, x[k + 2], S34, 0xc4ac5665); a = II(a, b, c, d, x[k + 0], S41, 0xf4292244); d = II(d, a, b, c, x[k + 7], S42, 0x432aff97); c = II(c, d, a, b, x[k + 14], S43, 0xab9423a7); b = II(b, c, d, a, x[k + 5], S44, 0xfc93a039); a = II(a, b, c, d, x[k + 12], S41, 0x655b59c3); d = II(d, a, b, c, x[k + 3], S42, 0x8f0ccc92); c = II(c, d, a, b, x[k + 10], S43, 0xffeff47d); b = II(b, c, d, a, x[k + 1], S44, 0x85845dd1); a = II(a, b, c, d, x[k + 8], S41, 0x6fa87e4f); d = II(d, a, b, c, x[k + 15], S42, 0xfe2ce6e0); c = II(c, d, a, b, x[k + 6], S43, 0xa3014314); b = II(b, c, d, a, x[k + 13], S44, 0x4e0811a1); a = II(a, b, c, d, x[k + 4], S41, 0xf7537e82); d = II(d, a, b, c, x[k + 11], S42, 0xbd3af235); c = II(c, d, a, b, x[k + 2], S43, 0x2ad7d2bb); b = II(b, c, d, a, x[k + 9], S44, 0xeb86d391); a = AddUnsigned(a, AA); b = AddUnsigned(b, BB); c = AddUnsigned(c, CC); d = AddUnsigned(d, DD); } const temp = WordToHex(a) + WordToHex(b) + WordToHex(c) + WordToHex(d); return temp.toLowerCase(); } // 将参数按字典序拼装为查询字符串 function buildSortedQuery(params) { const sortedKeys = Object.keys(params || {}).sort(); return sortedKeys.map(key => `${key}=${params[key] !== undefined ? params[key] : ''}`).join('&'); } // 调试开关:如需打印签名原串和 sig,临时改为 true const DEBUG_AMAP_SIG = false; // 构造高德 API 签名(正确规则:参数字符串 + 私钥,不含路径) function buildAmapSig(params) { const query = buildSortedQuery(params); const raw = `${query}${AMAP_SECURITY_KEY}`; if (DEBUG_AMAP_SIG) { console.log('[AMap Sig Debug] query:', query, 'raw:', raw, 'sig:', md5(raw)); } return md5(raw); } // 过滤掉 undefined / null 参数,避免签名与请求不一致 function sanitizeParams(obj = {}) { const cleaned = {}; Object.keys(obj).forEach((k) => { const v = obj[k]; if (v !== undefined && v !== null) { cleaned[k] = v; } }); return cleaned; } // 统一的高德 API 请求 function requestAmap(path, params = {}) { const normalizedPath = path.startsWith('/') ? path : `/${path}`; const baseParams = sanitizeParams({ key: AMAP_KEY, ...params }); const sig = buildAmapSig(baseParams); const queryParams = sanitizeParams({ ...baseParams, sig }); return new Promise((resolve, reject) => { uni.request({ url: `${AMAP_BASE_URL}${normalizedPath}`, method: 'GET', data: queryParams, success: (res) => { if (res.data && res.data.status === '1') { resolve(res.data); } else { reject(res.data || { message: 'AMap request failed' }); } }, fail: reject }); }); } // 将 WGS84 转 GCJ02(支付宝 my.getLocation 返回 WGS84) async function convertToGcj02(longitude, latitude) { if (!isAlipayEnv) { return { longitude, latitude }; } try { const data = await requestAmap('/assistant/coordinate/convert', { locations: `${longitude},${latitude}`, coordsys: 'gps' // gps 即 WGS84 }); const locStr = (data.locations || '').split(';')[0] || ''; const [lngStr, latStr] = locStr.split(','); const lng = Number(lngStr); const lat = Number(latStr); if (!isNaN(lng) && !isNaN(lat)) { return { longitude: lng, latitude: lat }; } } catch (err) { console.warn('坐标转换失败,使用原始坐标:', err); } return { longitude, latitude }; } // 内联腾讯地图SDK核心代码 const QQMapWX = (function() { // 错误配置 const ERROR_CONF = { KEY_ERR: 311, KEY_ERR_MSG: 'key格式错误', PARAM_ERR: 310, PARAM_ERR_MSG: '请求参数信息有误', SYSTEM_ERR: 600, SYSTEM_ERR_MSG: '系统错误', WX_ERR_CODE: 1000, WX_OK_CODE: 200 }; // API基础URL const BASE_URL = 'https://apis.map.qq.com/ws/'; const URL_SEARCH = BASE_URL + 'place/v1/search'; const URL_SUGGESTION = BASE_URL + 'place/v1/suggestion'; const URL_GET_GEOCODER = BASE_URL + 'geocoder/v1/'; const URL_DISTANCE = BASE_URL + 'distance/v1/'; // 工具函数 const Utils = { // 获取location参数 getLocationParam(location) { if (typeof location == 'string') { const locationArr = location.split(','); if (locationArr.length === 2) { location = { latitude: location.split(',')[0], longitude: location.split(',')[1] }; } else { location = {}; } } return location; }, // 验证location值 checkLocation(param) { const location = this.getLocationParam(param.location); if (!location || !location.latitude || !location.longitude) { const errconf = this.buildErrorConfig(ERROR_CONF.PARAM_ERR, ERROR_CONF.PARAM_ERR_MSG + ' location参数格式有误'); param.fail(errconf); param.complete(errconf); return false; } return true; }, // 构造错误数据结构 buildErrorConfig(errCode, errMsg) { return { status: errCode, message: errMsg }; }, // 回调函数默认处理 polyfillParam(param) { param.success = param.success || function () { }; param.fail = param.fail || function () { }; param.complete = param.complete || function () { }; }, // 处理用户参数是否传入坐标进行不同的处理 locationProcess(param, locationsuccess, locationfail, locationcomplete) { const that = this; locationfail = locationfail || function (res) { res.statusCode = ERROR_CONF.WX_ERR_CODE; param.fail(that.buildErrorConfig(ERROR_CONF.WX_ERR_CODE, res.errMsg)); }; locationcomplete = locationcomplete || function (res) { if (res.statusCode == ERROR_CONF.WX_ERR_CODE) { param.complete(that.buildErrorConfig(ERROR_CONF.WX_ERR_CODE, res.errMsg)); } }; if (!param.location) { // 微信小程序使用 wx.getLocation // #ifdef MP-WEIXIN wx.getLocation({ type: 'gcj02', success: locationsuccess, fail: locationfail, complete: locationcomplete }); // #endif // 支付宝小程序使用 my.getLocation(不支持 type) // #ifdef MP-ALIPAY my.getLocation({ success: (res) => { // 确保经纬度是数字类型 const result = { ...res, longitude: Number(res.longitude), latitude: Number(res.latitude) }; locationsuccess(result); }, fail: locationfail, complete: locationcomplete }); // #endif // 其他平台使用 uni.getLocation // #ifndef MP-WEIXIN // #ifndef MP-ALIPAY uni.getLocation({ type: 'gcj02', success: locationsuccess, fail: locationfail, complete: locationcomplete }); // #endif // #endif } else if (that.checkLocation(param)) { const location = Utils.getLocationParam(param.location); locationsuccess(location); } }, // 构造腾讯地图API请求参数(跨平台兼容) buildWxRequestConfig(param, options, feature) { const that = this; // GET请求不需要设置content-type,uni.request会自动处理 options.header = options.header || {}; options.method = 'GET'; options.success = function (res) { const data = res.data; if (data.status === 0) { that.handleData(param, data, feature); } else { param.fail(data); } }; options.fail = function (res) { res.statusCode = ERROR_CONF.WX_ERR_CODE; param.fail(that.buildErrorConfig(ERROR_CONF.WX_ERR_CODE, res.errMsg)); }; options.complete = function (res) { const statusCode = +res.statusCode; switch(statusCode) { case ERROR_CONF.WX_ERR_CODE: { param.complete(that.buildErrorConfig(ERROR_CONF.WX_ERR_CODE, res.errMsg)); break; } case ERROR_CONF.WX_OK_CODE: { const data = res.data; if (data.status === 0) { param.complete(data); } else { param.complete(that.buildErrorConfig(data.status, data.message)); } break; } default:{ param.complete(that.buildErrorConfig(ERROR_CONF.SYSTEM_ERR, ERROR_CONF.SYSTEM_ERR_MSG)); } } }; return options; }, // 数据处理函数 handleData(param, data, feature) { if (feature == 'search') { const searchResult = data.data; const searchSimplify = []; for (let i = 0; i < searchResult.length; i++) { searchSimplify.push({ id: searchResult[i].id || null, title: searchResult[i].title || null, latitude: searchResult[i].location && searchResult[i].location.lat || null, longitude: searchResult[i].location && searchResult[i].location.lng || null, address: searchResult[i].address || null, category: searchResult[i].category || null, tel: searchResult[i].tel || null, adcode: searchResult[i].ad_info && searchResult[i].ad_info.adcode || null, city: searchResult[i].ad_info && searchResult[i].ad_info.city || null, district: searchResult[i].ad_info && searchResult[i].ad_info.district || null, province: searchResult[i].ad_info && searchResult[i].ad_info.province || null }); } param.success(data, { searchResult: searchResult, searchSimplify: searchSimplify }); } else if (feature == 'suggest') { const suggestResult = data.data; const suggestSimplify = []; for (let i = 0; i < suggestResult.length; i++) { suggestSimplify.push({ adcode: suggestResult[i].adcode || null, address: suggestResult[i].address || null, category: suggestResult[i].category || null, city: suggestResult[i].city || null, district: suggestResult[i].district || null, id: suggestResult[i].id || null, latitude: suggestResult[i].location && suggestResult[i].location.lat || null, longitude: suggestResult[i].location && suggestResult[i].location.lng || null, province: suggestResult[i].province || null, title: suggestResult[i].title || null, type: suggestResult[i].type || null }); } param.success(data, { suggestResult: suggestResult, suggestSimplify: suggestSimplify }); } else if (feature == 'reverseGeocoder') { const reverseGeocoderResult = data.result; const reverseGeocoderSimplify = { address: reverseGeocoderResult.address || null, latitude: reverseGeocoderResult.location && reverseGeocoderResult.location.lat || null, longitude: reverseGeocoderResult.location && reverseGeocoderResult.location.lng || null, adcode: reverseGeocoderResult.ad_info && reverseGeocoderResult.ad_info.adcode || null, city: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.city || null, district: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.district || null, nation: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.nation || null, province: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.province || null, street: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.street || null, street_number: reverseGeocoderResult.address_component && reverseGeocoderResult.address_component.street_number || null, recommend: reverseGeocoderResult.formatted_addresses && reverseGeocoderResult.formatted_addresses.recommend || null, rough: reverseGeocoderResult.formatted_addresses && reverseGeocoderResult.formatted_addresses.rough || null }; param.success(data, { reverseGeocoderResult: reverseGeocoderResult, reverseGeocoderSimplify: reverseGeocoderSimplify }); } else if (feature == 'calculateDistance') { const calculateDistanceResult = data.result.elements; const distance = []; for (let i = 0; i < calculateDistanceResult.length; i++){ distance.push(calculateDistanceResult[i].distance); } param.success(data, { calculateDistanceResult: calculateDistanceResult, distance: distance }); } else { param.success(data); } } }; // QQMapWX类 class QQMapWX { constructor(options) { if (!options.key) { throw Error('key值不能为空'); } this.key = options.key; } // 逆地址解析 reverseGeocoder(options) { const that = this; options = options || {}; Utils.polyfillParam(options); const requestParam = { coord_type: options.coord_type || 5, get_poi: options.get_poi || 0, output: 'json', key: that.key }; if (options.poi_options) { requestParam.poi_options = options.poi_options; } const locationsuccess = function (result) { requestParam.location = result.latitude + ',' + result.longitude; // 调用腾讯地图API - 逆地理编码接口 // 使用 uni.request 跨平台API,在支付宝小程序中自动转换为 my.request uni.request(Utils.buildWxRequestConfig(options, { url: URL_GET_GEOCODER, // 腾讯地图API地址:https://apis.map.qq.com/ws/geocoder/v1/ data: requestParam }, 'reverseGeocoder')); }; Utils.locationProcess(options, locationsuccess); } // POI周边检索 search(options) { const that = this; options = options || {}; Utils.polyfillParam(options); if (!options.keyword) { const errconf = Utils.buildErrorConfig(ERROR_CONF.PARAM_ERR, ERROR_CONF.PARAM_ERR_MSG + ' keyword参数格式有误'); options.fail(errconf); options.complete(errconf); return; } const requestParam = { keyword: options.keyword, orderby: options.orderby || '_distance', page_size: options.page_size || 10, page_index: options.page_index || 1, output: 'json', key: that.key }; if (options.address_format) { requestParam.address_format = options.address_format; } if (options.filter) { requestParam.filter = options.filter; } const distance = options.distance || "1000"; const auto_extend = options.auto_extend || 1; const locationsuccess = function (result) { requestParam.boundary = "nearby(" + result.latitude + "," + result.longitude + "," + distance + "," + auto_extend + ")"; // 调用腾讯地图API - POI周边检索接口 // 使用 uni.request 跨平台API,在支付宝小程序中自动转换为 my.request uni.request(Utils.buildWxRequestConfig(options, { url: URL_SEARCH, // 腾讯地图API地址:https://apis.map.qq.com/ws/place/v1/search data: requestParam }, 'search')); }; Utils.locationProcess(options, locationsuccess); } // sug模糊检索 getSuggestion(options) { const that = this; options = options || {}; Utils.polyfillParam(options); if (!options.keyword) { const errconf = Utils.buildErrorConfig(ERROR_CONF.PARAM_ERR, ERROR_CONF.PARAM_ERR_MSG + ' keyword参数格式有误'); options.fail(errconf); options.complete(errconf); return; } const requestParam = { keyword: options.keyword, region: options.region || '全国', region_fix: options.region_fix || 0, policy: options.policy || 0, page_size: options.page_size || 10, page_index: options.page_index || 1, get_subpois: options.get_subpois || 0, output: 'json', key: that.key }; if (options.address_format) { requestParam.address_format = options.address_format; } if (options.filter) { requestParam.filter = options.filter; } if (options.location) { const locationsuccess = function (result) { requestParam.location = result.latitude + ',' + result.longitude; // 调用腾讯地图API - 地点搜索建议接口 // 使用 uni.request 跨平台API,在支付宝小程序中自动转换为 my.request uni.request(Utils.buildWxRequestConfig(options, { url: URL_SUGGESTION, // 腾讯地图API地址:https://apis.map.qq.com/ws/place/v1/suggestion data: requestParam }, "suggest")); }; Utils.locationProcess(options, locationsuccess); } else { // 调用腾讯地图API - 地点搜索建议接口(无位置信息) // 使用 uni.request 跨平台API,在支付宝小程序中自动转换为 my.request uni.request(Utils.buildWxRequestConfig(options, { url: URL_SUGGESTION, // 腾讯地图API地址:https://apis.map.qq.com/ws/place/v1/suggestion data: requestParam }, "suggest")); } } // 距离计算 calculateDistance(options) { const that = this; options = options || {}; Utils.polyfillParam(options); if (!options.to) { const errconf = Utils.buildErrorConfig(ERROR_CONF.PARAM_ERR, ERROR_CONF.PARAM_ERR_MSG + ' to参数格式有误'); options.fail(errconf); options.complete(errconf); return; } const requestParam = { mode: options.mode || 'walking', to: options.to, output: 'json', key: that.key }; if (options.from) { options.location = options.from; } const locationsuccess = function (result) { requestParam.from = result.latitude + ',' + result.longitude; // 调用腾讯地图API - 距离计算接口 // 使用 uni.request 跨平台API,在支付宝小程序中自动转换为 my.request uni.request(Utils.buildWxRequestConfig(options, { url: URL_DISTANCE, // 腾讯地图API地址:https://apis.map.qq.com/ws/distance/v1/ data: requestParam }, 'calculateDistance')); }; Utils.locationProcess(options, locationsuccess); } } return QQMapWX; })(); // 全局QQMap实例 let qqmapInstance = null; // 初始化腾讯地图SDK function initQQMap() { if (isAlipayEnv) { // 支付宝端改用高德地图服务 return null; } if (!qqmapInstance) { try { qqmapInstance = new QQMapWX({ key: QQMAP_KEY }); console.log('腾讯地图SDK初始化成功'); } catch (err) { console.error('初始化腾讯地图SDK失败:', err); } } return qqmapInstance; } // 获取腾讯地图SDK实例 function getQQMapInstance() { if (isAlipayEnv) { return null; } return qqmapInstance || initQQMap(); } // 获取用户位置(统一处理小程序定位授权 && 实际定位) function getUserLocation() { return new Promise((resolve, reject) => { // 小程序端优先通过 getSetting 判断权限状态 // 这里只考虑 mp-weixin 场景,其它平台回退到直接调用 getLocation // #ifdef MP-WEIXIN uni.getSetting({ success: (settingRes) => { const authSetting = settingRes.authSetting || {}; // 兼容多种定位 scope(微信近几次版本变更比较多) const hasUserLocation = Object.prototype.hasOwnProperty.call(authSetting, 'scope.userLocation'); const userLocationAuth = authSetting['scope.userLocation']; const fuzzyLocationAuth = authSetting['scope.userFuzzyLocation']; const bgLocationAuth = authSetting['scope.userLocationBackground']; // 已明确拒绝定位 if ( userLocationAuth === false || fuzzyLocationAuth === false || bgLocationAuth === false ) { uni.showModal({ title: getPermissionText('locationTitle'), content: getPermissionText('locationNeed'), confirmText: getPermissionText('goToSettings'), cancelText: getPermissionText('later'), success: (modalRes) => { if (modalRes.confirm) { uni.openSetting({}); } } }); reject({ code: 'LOCATION_DENIED', errMsg: 'user denied location permission' }); return; } const doGetLocation = () => { wx.getLocation({ type: 'gcj02', success: (res) => { // 确保经纬度是数字类型 const longitude = parseFloat(Number(res.longitude).toFixed(5)); const latitude = parseFloat(Number(res.latitude).toFixed(5)); console.log('地址获取成功'); resolve({ longitude, latitude }); }, fail: (error) => { console.error('获取位置失败:', error); reject(error); } }); }; // 未显式授权时,主动申请一次权限(老版本 scope 为 scope.userLocation) if (!hasUserLocation || userLocationAuth === undefined) { uni.authorize({ scope: 'scope.userLocation', success: () => { doGetLocation(); }, fail: (authErr) => { console.error('定位授权失败:', authErr); uni.showModal({ title: getPermissionText('locationTitle'), content: getPermissionText('locationDenied'), confirmText: getPermissionText('gotIt'), showCancel: false }); reject({ code: 'LOCATION_AUTH_FAIL', errMsg: authErr.errMsg || 'authorize location fail' }); } }); } else { // 已授权,直接获取定位 doGetLocation(); } }, fail: (err) => { console.warn('获取授权设置失败,直接尝试定位:', err); wx.getLocation({ type: 'gcj02', success: (res) => { // 确保经纬度是数字类型 const longitude = parseFloat(Number(res.longitude).toFixed(5)); const latitude = parseFloat(Number(res.latitude).toFixed(5)); console.log('地址获取成功'); resolve({ longitude, latitude }); }, fail: (error) => { console.error('获取位置失败:', error); reject(error); } }); } }); // #endif // 支付宝小程序:使用 my.getLocation API,不支持 type 参数 // #ifdef MP-ALIPAY // 支付宝小程序权限检查 my.getSetting({ success: (settingRes) => { const authSetting = settingRes.authSetting || {}; const hasLocationAuth = Object.prototype.hasOwnProperty.call(authSetting, 'location'); const locationAuth = authSetting['location']; // 已明确拒绝定位 if (locationAuth === false) { my.showModal({ title: getPermissionText('locationTitle'), content: getPermissionText('locationNeed'), confirmText: getPermissionText('goToSettings'), cancelText: getPermissionText('later'), success: (modalRes) => { if (modalRes.confirm) { my.openSetting({}); } } }); reject({ code: 'LOCATION_DENIED', errMsg: 'user denied location permission' }); return; } const doGetLocation = () => { // 支付宝小程序使用 my.getLocation,不支持 type 参数 my.getLocation({ success: (res) => { // 支付宝小程序返回的经纬度可能是字符串,需要先转换为数字 // 注意:支付宝返回的是 WGS84 坐标系,如需 GCJ02 需要转换 const longitude = parseFloat(Number(res.longitude).toFixed(5)); const latitude = parseFloat(Number(res.latitude).toFixed(5)); console.log('支付宝地址获取成功'); resolve({ longitude, latitude }); }, fail: (error) => { console.error('获取位置失败:', error); reject(error); } }); }; // 未显式授权时,主动申请一次权限 if (!hasLocationAuth || locationAuth === undefined) { my.authorize({ scope: 'scope.userLocation', success: () => { doGetLocation(); }, fail: (authErr) => { console.error('定位授权失败:', authErr); my.showModal({ title: getPermissionText('locationTitle'), content: getPermissionText('locationDenied'), confirmText: getPermissionText('gotIt'), showCancel: false }); reject({ code: 'LOCATION_AUTH_FAIL', errMsg: authErr.errorMessage || 'authorize location fail' }); } }); } else { // 已授权,直接获取定位 doGetLocation(); } }, fail: (err) => { console.warn('获取授权设置失败,直接尝试定位:', err); // 支付宝小程序使用 my.getLocation,不支持 type 参数 my.getLocation({ success: (res) => { // 支付宝小程序返回的经纬度可能是字符串,需要先转换为数字 const longitude = parseFloat(Number(res.longitude).toFixed(5)); const latitude = parseFloat(Number(res.latitude).toFixed(5)); console.log('支付宝地址获取成功'); resolve({ longitude, latitude }); }, fail: (error) => { console.error('获取位置失败:', error); reject(error); } }); } }); // #endif // 其他非微信、非支付宝小程序平台 // #ifndef MP-WEIXIN // #ifndef MP-ALIPAY uni.getLocation({ type: 'gcj02', success: (res) => { // 确保经纬度是数字类型 const longitude = parseFloat(Number(res.longitude).toFixed(5)); const latitude = parseFloat(Number(res.latitude).toFixed(5)); console.log('地址获取成功'); resolve({ longitude, latitude }); }, fail: (error) => { console.error('获取位置失败:', error); reject(error); } }); // #endif // #endif }); } // 逆地理编码 - 根据经纬度获取地址信息 function getRegeo(longitude, latitude) { return new Promise((resolve, reject) => { // 支付宝端使用高德逆地理编码 if (isAlipayEnv) { (async () => { try { const gcj = await convertToGcj02(longitude, latitude); const data = await requestAmap('/geocode/regeo', { location: `${gcj.longitude},${gcj.latitude}`, extensions: 'all', batch: 'false' }); const info = data.regeocode || {}; const comp = info.addressComponent || {}; resolve({ success: true, data: { formatted_address: info.formatted_address || '', addressComponent: { city: comp.city || comp.province || '', district: comp.district || '', province: comp.province || '', street: comp.streetNumber?.street || comp.street || '', street_number: comp.streetNumber?.number || comp.streetNumber?.street_number || '' } } }); } catch (error) { console.error('高德逆地理编码失败:', error); reject({ success: false, message: error.message || '逆地理编码失败' }); } })(); return; } const qqmap = getQQMapInstance(); if (!qqmap) { reject({ success: false, message: '腾讯地图SDK未初始化' }); return; } qqmap.reverseGeocoder({ location: { latitude, longitude }, success: (data, result) => { // 官方SDK返回的数据结构:data是原始数据,result是简化数据 const reverseGeocoderSimplify = result.reverseGeocoderSimplify; resolve({ success: true, data: { formatted_address: reverseGeocoderSimplify.address, addressComponent: { city: reverseGeocoderSimplify.city, district: reverseGeocoderSimplify.district, province: reverseGeocoderSimplify.province, street: reverseGeocoderSimplify.street, street_number: reverseGeocoderSimplify.street_number } } }); }, fail: (error) => { console.error('逆地理编码失败:', error); reject({ success: false, message: error.message || '逆地理编码失败' }); } }); }); } // 搜索周边POI function getPoiAround(longitude, latitude, keyword = '', radius = 1000) { return new Promise((resolve, reject) => { // 支付宝端使用高德周边搜索 if (isAlipayEnv) { (async () => { try { const gcj = await convertToGcj02(longitude, latitude); const data = await requestAmap('/place/around', { location: `${gcj.longitude},${gcj.latitude}`, keywords: keyword, radius, offset: 20, page: 1, extensions: 'base', sortrule: 'distance' }); const pois = data.pois || []; const normalized = pois.map((item, idx) => ({ id: item.id || idx + 1, title: item.name || '', latitude: Number(item.location?.split(',')[1] || item.location?.lat || 0), longitude: Number(item.location?.split(',')[0] || item.location?.lng || 0), address: item.address || '', category: item.type || '', tel: item.tel || '', city: item.cityname || '', district: item.adname || '', province: item.pname || '' })); resolve({ success: true, data: normalized }); } catch (error) { console.error('高德周边搜索失败:', error); reject({ success: false, message: error.message || '搜索POI失败' }); } })(); return; } const qqmap = getQQMapInstance(); if (!qqmap) { reject({ success: false, message: '腾讯地图SDK未初始化' }); return; } qqmap.search({ keyword: keyword, location: { latitude, longitude }, distance: radius, success: (data, result) => { const searchSimplify = result.searchSimplify; resolve({ success: true, data: searchSimplify }); }, fail: (error) => { console.error('搜索POI失败:', error); reject({ success: false, message: error.message || '搜索POI失败' }); } }); }); } // 计算距离(异步) function calculateDistance(from, to) { return new Promise((resolve, reject) => { // 支付宝端使用高德距离测量接口 if (isAlipayEnv) { // to 支持数组 const targets = Array.isArray(to) ? to : [to]; if (!targets.length) { reject({ success: false, message: '缺少目标坐标' }); return; } const doRequest = async (originPoint) => { if (!originPoint || originPoint.longitude === undefined || originPoint.latitude === undefined) { reject({ success: false, message: '缺少起点坐标' }); return; } try { const originGcj = await convertToGcj02(originPoint.longitude || originPoint.lng, originPoint.latitude || originPoint.lat); const convertedTargets = await Promise.all(targets.map(async (item) => { const lng = item.longitude || item.lng; const lat = item.latitude || item.lat; const res = await convertToGcj02(lng, lat); return `${res.longitude},${res.latitude}`; })); const origins = convertedTargets.join('|'); const destination = `${originGcj.longitude},${originGcj.latitude}`; const data = await requestAmap('/distance', { origins, destination, type: 3 // 3:步行距离 }); const results = data.results || []; const distance = results.map(r => Number(r.distance || 0)); resolve({ success: true, data: distance }); } catch (error) { console.error('高德距离计算失败:', error); reject({ success: false, message: error.message || '计算距离失败' }); } }; if (from && typeof from === 'object') { doRequest(from); } else { // 未传入起点时,使用用户当前定位 getUserLocation().then(loc => { doRequest({ longitude: loc.longitude, latitude: loc.latitude }); }).catch(err => { reject({ success: false, message: err.message || '获取起点位置失败' }); }); } return; } const qqmap = getQQMapInstance(); if (!qqmap) { reject({ success: false, message: '腾讯地图SDK未初始化' }); return; } qqmap.calculateDistance({ from: from, to: to, mode: 'walking', success: (data, result) => { const distance = result.distance; resolve({ success: true, data: distance }); }, fail: (error) => { console.error('计算距离失败:', error); reject({ success: false, message: error.message || '计算距离失败' }); } }); }); } // 计算距离(同步,使用球面距离公式) function calculateDistanceSync(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 Math.round(R * c); // 返回距离,单位为米 } // 关键词提示 function getSuggestion(keyword, region = '全国') { return new Promise((resolve, reject) => { // 支付宝端使用高德输入提示 if (isAlipayEnv) { requestAmap('/assistant/inputtips', { keywords: keyword, city: region === '全国' ? undefined : region, datatype: 'all' }).then((data) => { const tips = data.tips || []; const normalized = tips.map((item, idx) => ({ id: item.id || idx + 1, title: item.name || '', adcode: item.adcode || '', address: item.address || '', category: item.type || '', city: item.city || '', district: item.district || '', latitude: item.location ? Number(item.location.split(',')[1]) : null, longitude: item.location ? Number(item.location.split(',')[0]) : null, province: item.province || '' })); resolve({ success: true, data: normalized }); }).catch((error) => { console.error('高德关键词提示失败:', error); reject({ success: false, message: error.message || '关键词提示失败' }); }); return; } const qqmap = getQQMapInstance(); if (!qqmap) { reject({ success: false, message: '腾讯地图SDK未初始化' }); return; } qqmap.getSuggestion({ keyword: keyword, region: region, success: (data, result) => { const suggestSimplify = result.suggestSimplify; resolve({ success: true, data: suggestSimplify }); }, fail: (error) => { console.error('关键词提示失败:', error); reject({ success: false, message: error.message || '关键词提示失败' }); } }); }); } // 导出函数 export { getUserLocation, getRegeo, getPoiAround, calculateDistance, calculateDistanceSync, getSuggestion, initQQMap, getQQMapInstance }; // 测试距离计算函数(开发调试用) export function testDistanceCalculation() { // 测试用例:北京天安门到故宫的距离(约1.5公里) const tiananmen = { lat: 39.908823, lng: 116.397470 }; const gugong = { lat: 39.916527, lng: 116.397128 }; const distance = calculateDistanceSync(tiananmen.lat, tiananmen.lng, gugong.lat, gugong.lng); console.log('天安门到故宫的距离:', distance, '米'); console.log('转换为公里:', (distance / 1000).toFixed(2), '公里'); return distance; }