diff --git a/common/config.js b/common/config.js index 5801610..80f6f94 100644 --- a/common/config.js +++ b/common/config.js @@ -20,4 +20,7 @@ switch (mode) { // 微信配置 const WECHAT_APPID = 'wx3ff67f2e2b0c62ca' // 从manifest.json中获取的appid -export { ROOTPATH, WECHAT_APPID } +// 天地图(H5 首页地图) +const TIANDITU_TK = '0c86c1c69a09ecaa5e9b3d0373fb67bf' + +export { ROOTPATH, WECHAT_APPID, TIANDITU_TK } diff --git a/common/coord.js b/common/coord.js new file mode 100644 index 0000000..7bae019 --- /dev/null +++ b/common/coord.js @@ -0,0 +1,68 @@ +/** + * 坐标转换:业务数据为腾讯/国测局 GCJ-02,天地图底图为 WGS84/CGCS2000 + */ +const PI = Math.PI +const A = 6378245.0 +const EE = 0.00669342162296594323 + +function outOfChina(lng, lat) { + return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271 +} + +function transformLat(lng, lat) { + let ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng)) + ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0 + ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0 + ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0 + return ret +} + +function transformLng(lng, lat) { + let ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng)) + ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0 + ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0 + ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0 + return ret +} + +/** GCJ-02 → WGS84(天地图展示用) */ +export function gcj02ToWgs84(lng, lat) { + lng = parseFloat(lng) + lat = parseFloat(lat) + if (isNaN(lng) || isNaN(lat)) return { lng, lat } + if (outOfChina(lng, lat)) return { lng, lat } + let dlat = transformLat(lng - 105.0, lat - 35.0) + let dlng = transformLng(lng - 105.0, lat - 35.0) + const radlat = lat / 180.0 * PI + let magic = Math.sin(radlat) + magic = 1 - EE * magic * magic + const sqrtmagic = Math.sqrt(magic) + dlat = (dlat * 180.0) / ((A * (1 - EE)) / (magic * sqrtmagic) * PI) + dlng = (dlng * 180.0) / (A / sqrtmagic * Math.cos(radlat) * PI) + const mglat = lat + dlat + const mglng = lng + dlng + return { lng: lng * 2 - mglng, lat: lat * 2 - mglat } +} + +/** WGS84 → GCJ-02 */ +export function wgs84ToGcj02(lng, lat) { + lng = parseFloat(lng) + lat = parseFloat(lat) + if (isNaN(lng) || isNaN(lat)) return { lng, lat } + if (outOfChina(lng, lat)) return { lng, lat } + let dlat = transformLat(lng - 105.0, lat - 35.0) + let dlng = transformLng(lng - 105.0, lat - 35.0) + const radlat = lat / 180.0 * PI + let magic = Math.sin(radlat) + magic = 1 - EE * magic * magic + const sqrtmagic = Math.sqrt(magic) + dlat = (dlat * 180.0) / ((A * (1 - EE)) / (magic * sqrtmagic) * PI) + dlng = (dlng * 180.0) / (A / sqrtmagic * Math.cos(radlat) * PI) + return { lng: lng + dlng, lat: lat + dlat } +} + +/** uni-app scale(约3-20) 转天地图 zoom(约1-18) */ +export function uniScaleToTdtZoom(scale) { + const s = parseInt(scale, 10) || 11 + return Math.max(3, Math.min(18, s + 1)) +} diff --git a/common/h5-asset.js b/common/h5-asset.js new file mode 100644 index 0000000..ffeb770 --- /dev/null +++ b/common/h5-asset.js @@ -0,0 +1,47 @@ +/** + * H5 静态资源转绝对 URL(兼容 manifest publicPath: /h5walksz/) + * 注意:天地图 T.Icon 不支持 data:/blob:,标注图必须使用 http(s) 地址 + */ +export function resolveH5AssetUrl(path) { + if (!path || typeof path !== 'string') return path + // data/blob 不能作为网络图片加载,交给 normalizeMarkerIconUrl 处理 + if (/^(https?:)/i.test(path)) return path + + let assetPath = path + if (!assetPath.startsWith('/')) { + assetPath = '/' + assetPath + } + + if (typeof window === 'undefined') { + const base = (typeof process !== 'undefined' && process.env && process.env.BASE_URL) || '/h5walksz/' + return base.replace(/\/$/, '') + assetPath + } + + let basePath = '' + if (typeof process !== 'undefined' && process.env && process.env.BASE_URL) { + basePath = process.env.BASE_URL.replace(/\/$/, '') + } + if (!basePath) { + const match = window.location.pathname.match(/^\/(h5walksz)(?:\/|$)/) + basePath = match ? '/' + match[1] : '' + } + return window.location.origin + basePath + assetPath +} + +/** 默认地图标注图(固定 http 路径,避免 webpack 内联为 data:) */ +export function getDefaultMarkerUrl() { + return resolveH5AssetUrl('/static/home-marker.png') +} + +/** + * 标注图标 URL 规范化(天地图仅支持 http/https) + */ +export function normalizeMarkerIconUrl(url) { + if (!url || /^data:|^blob:/i.test(url)) { + return getDefaultMarkerUrl() + } + if (/^https?:\/\//i.test(url)) { + return url + } + return resolveH5AssetUrl(url) +} diff --git a/common/tianditu-h5.js b/common/tianditu-h5.js new file mode 100644 index 0000000..e0c2c21 --- /dev/null +++ b/common/tianditu-h5.js @@ -0,0 +1,176 @@ +/** + * 天地图 H5 封装(仅浏览器端) + */ +import { getDefaultMarkerUrl, normalizeMarkerIconUrl } from '@/common/h5-asset.js' + +const API_BASE = 'https://api.tianditu.gov.cn/api?v=4.0' + +let scriptPromise = null + +export function loadTiandituScript(tk) { + if (typeof window === 'undefined') { + return Promise.reject(new Error('非浏览器环境')) + } + if (window.T && window.T.Map) { + return Promise.resolve(window.T) + } + if (scriptPromise) return scriptPromise + scriptPromise = new Promise((resolve, reject) => { + const existed = document.querySelector('script[data-tianditu-api]') + if (existed) { + existed.addEventListener('load', () => resolve(window.T)) + existed.addEventListener('error', reject) + return + } + const script = document.createElement('script') + script.type = 'text/javascript' + script.src = `${API_BASE}&tk=${tk}` + script.setAttribute('data-tianditu-api', '1') + script.onload = () => { + if (window.T) resolve(window.T) + else reject(new Error('天地图 API 加载失败')) + } + script.onerror = () => reject(new Error('天地图脚本加载失败')) + document.head.appendChild(script) + }) + return scriptPromise +} + +export class TiandituMapHelper { + constructor(options) { + this.containerId = options.containerId + this.tk = options.tk + this.onMarkerClick = options.onMarkerClick || (() => {}) + this.map = null + this.pointMarkers = [] + this.userMarker = null + this._defaultIconUrl = getDefaultMarkerUrl() + } + + async init(lng, lat, zoom) { + await loadTiandituScript(this.tk) + await new Promise((r) => { + if (typeof requestAnimationFrame === 'function') requestAnimationFrame(r) + else setTimeout(r, 50) + }) + const el = document.getElementById(this.containerId) + if (!el) throw new Error('地图容器不存在: ' + this.containerId) + const T = window.T + this.map = new T.Map(this.containerId) + this.map.centerAndZoom(new T.LngLat(lng, lat), zoom) + if (typeof this.map.enableScrollWheelZoom === 'function') { + this.map.enableScrollWheelZoom() + } + await new Promise((r) => setTimeout(r, 200)) + if (typeof this.map.checkResize === 'function') { + this.map.checkResize() + } + return this.map + } + + clearPointMarkers() { + if (!this.map) return + this.pointMarkers.forEach((m) => { + try { + this.map.removeOverLay(m) + } catch (e) {} + }) + this.pointMarkers = [] + } + + resolveIconUrl(rawUrl) { + const iconUrl = normalizeMarkerIconUrl(rawUrl || this._defaultIconUrl) + if (!/^https?:\/\//i.test(iconUrl)) { + return this._defaultIconUrl + } + return iconUrl + } + + async setPointMarkers(list) { + if (!this.map || !window.T) return + const T = window.T + this.clearPointMarkers() + if (!list || !list.length) return + + for (let i = 0; i < list.length; i++) { + const item = list[i] + const lng = parseFloat(item.lng) + const lat = parseFloat(item.lat) + if (isNaN(lng) || isNaN(lat)) continue + + const iconUrl = this.resolveIconUrl(item.iconUrl) + const point = new T.LngLat(lng, lat) + const MARKER_SIZE = 44 + const icon = new T.Icon({ + iconUrl, + iconSize: new T.Point(MARKER_SIZE, MARKER_SIZE), + iconAnchor: new T.Point(MARKER_SIZE / 2, MARKER_SIZE) + }) + const marker = new T.Marker(point, { icon }) + if (typeof marker.setZIndexOffset === 'function') { + marker.setZIndexOffset(1000) + } + const pointId = item.id + marker.addEventListener('click', () => { + this.onMarkerClick({ markerId: pointId }) + }) + this.map.addOverLay(marker) + this.pointMarkers.push(marker) + } + + if (typeof this.map.checkResize === 'function') { + setTimeout(() => this.map.checkResize(), 150) + } + } + + setUserMarker(lng, lat) { + if (!this.map || lng == null || lat == null) return + const T = window.T + const point = new T.LngLat(lng, lat) + if (this.userMarker) { + try { + this.map.removeOverLay(this.userMarker) + } catch (e) {} + this.userMarker = null + } + this.userMarker = new T.Circle(point, 60, { + color: '#1791fc', + weight: 2, + opacity: 0.9, + fillColor: '#1791fc', + fillOpacity: 0.25, + lineStyle: 'solid' + }) + this.map.addOverLay(this.userMarker) + } + + centerAndZoom(lng, lat, zoom) { + if (!this.map) return + this.map.centerAndZoom(new T.LngLat(lng, lat), zoom) + } + + panTo(lng, lat) { + if (!this.map) return + this.map.panTo(new T.LngLat(lng, lat)) + } + + getZoom() { + return this.map ? this.map.getZoom() : null + } + + destroy() { + this.clearPointMarkers() + if (this.userMarker && this.map) { + try { + this.map.removeOverLay(this.userMarker) + } catch (e) {} + } + this.userMarker = null + this.map = null + } +} + +export function buildTiandituNavUrl(wgsLng, wgsLat, name) { + const n = encodeURIComponent(name || '目的地') + return `https://map.tianditu.gov.cn/?l=${wgsLng}&lat=${wgsLat}&name=${n}` +} diff --git a/common/util.js b/common/util.js index dc9a9b4..cfeaa73 100644 --- a/common/util.js +++ b/common/util.js @@ -4,6 +4,11 @@ */ import moment from 'moment'; import { lang } from 'moment'; +// #ifdef H5 +import { isWechatBrowser, openWechatLocation } from '@/common/wechat-jssdk.js' +import { gcj02ToWgs84 } from '@/common/coord.js' +import { buildTiandituNavUrl } from '@/common/tianditu-h5.js' +// #endif const base64ToFile = (dataurl, filename = 'file') => { let arr = dataurl.split(',') @@ -163,17 +168,19 @@ const toMapAPP = (lat,lng,name) => { }) // #endif // #ifdef H5 - // H5端使用腾讯地图URL导航 const lat1 = parseFloat(lat) const lng1 = parseFloat(lng) - const nameEncoded = encodeURIComponent(name || '目的地') - // 使用腾讯地图路线规划URL - const url = `https://apis.map.qq.com/uri/v1/routeplan?type=drive&to=${lat1},${lng1}&tocoord=gcj02&toname=${nameEncoded}&referer=myapp` - // 在新窗口打开 + if (isWechatBrowser()) { + openWechatLocation(lat1, lng1, name).catch(() => { + toast('打开导航失败,请稍后重试') + }) + return + } + const wgs = gcj02ToWgs84(lng1, lat1) + const url = buildTiandituNavUrl(wgs.lng, wgs.lat, name) if (typeof window !== 'undefined') { window.open(url, '_blank') } else { - // 如果window不存在,使用uni API plus && plus.runtime.openURL(url) } // #endif diff --git a/common/wechat-jssdk.js b/common/wechat-jssdk.js index 1906702..68a806a 100644 --- a/common/wechat-jssdk.js +++ b/common/wechat-jssdk.js @@ -1,134 +1,293 @@ /* - * 微信JS-SDK工具 - * 用于H5端的微信分享功能 + * 微信公众号 H5 - 微信 JS-SDK + * 签名接口 /wechat-share 需要 Bearer token,须在登录成功后调用 */ import { ROOTPATH, WECHAT_APPID } from '@/common/config.js' -let wx = null +const DEFAULT_JS_API = [ + 'getLocation', + 'openLocation', + 'updateAppMessageShareData', + 'updateTimelineShareData' +] + +const SIGNATURE_APIS = [ + '/api/mobile/user/wechat-share', + '/api/mobile/user/wechat-login-url' +] + +let wxInstance = null +let wxInitPromise = null + +export function isWechatBrowser() { + return typeof navigator !== 'undefined' && /MicroMessenger/i.test(navigator.userAgent) +} + +export function getStoredToken() { + const walksz_lifeData = uni.getStorageSync('walksz_lifeData') || {} + return walksz_lifeData.vuex_token || '' +} + +export function resetWxJssdk() { + wxInitPromise = null +} + +/** + * 参与签名的页面 URL(须与后端 wechat-share 入参 url 完全一致) + * 后端常按 https://域名/h5walksz/ 带尾斜杠签名 + */ +export function getSignPageUrl() { + let url = window.location.href.split('#')[0] + if (url.endsWith('/h5walksz')) { + url += '/' + } + return url +} + +function buildJsApiList(serverList) { + const required = [ + 'getLocation', + 'openLocation', + 'updateAppMessageShareData', + 'updateTimelineShareData', + 'onMenuShareTimeline', + 'onMenuShareAppMessage' + ] + const merged = [...(Array.isArray(serverList) ? serverList : []), ...required] + return Array.from(new Set(merged)) +} + +/** + * 解析 wechat-share 返回(根级含 appId/nonceStr/timestamp/signature/jsApiList) + */ +export function normalizeSignaturePayload(body) { + if (!body) return null + let payload = body + if (typeof payload === 'string') { + try { + payload = JSON.parse(payload) + } catch (e) { + return null + } + } + if (payload.data && payload.data.signature) { + payload = payload.data + } + if (!payload.signature) return null + const timestamp = payload.timestamp || payload.timeStamp + const nonceStr = payload.nonceStr || payload.noncestr + if (!timestamp && timestamp !== 0) return null + if (!nonceStr) return null + return { + signature: payload.signature, + timestamp: String(timestamp), + nonceStr, + appId: payload.appId || payload.appid || WECHAT_APPID, + jsApiList: buildJsApiList(payload.jsApiList), + signUrl: payload.url || '' + } +} + +function getAuthHeader() { + const token = getStoredToken() + return token ? { Authorization: `Bearer ${token}` } : {} +} + +function parseWxSignatureResponse(res) { + if (!res || res.statusCode !== 200) return null + let body = res.data + if (!body) return null + if (body.errcode !== undefined && body.errcode !== 0) { + console.warn('微信签名接口返回错误:', body.errcode, body.errmsg || body.message) + return null + } + return normalizeSignaturePayload(body) +} -// 动态加载微信JS-SDK const loadWxSDK = () => { return new Promise((resolve, reject) => { - // #ifdef H5 - if (typeof window !== 'undefined' && !window.wx) { - const script = document.createElement('script') - script.src = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js' - script.onload = () => { - wx = window.wx - resolve(wx) - } - script.onerror = () => { - reject(new Error('加载微信JS-SDK失败')) - } - document.head.appendChild(script) - } else if (window.wx) { - wx = window.wx - resolve(wx) - } else { - reject(new Error('不支持的环境')) + if (typeof window === 'undefined') { + reject(new Error('非浏览器环境')) + return } - // #endif - // #ifndef H5 - resolve(null) - // #endif + if (window.wx) { + wxInstance = window.wx + resolve(wxInstance) + return + } + const script = document.createElement('script') + script.src = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js' + script.onload = () => { + wxInstance = window.wx + resolve(wxInstance) + } + script.onerror = () => reject(new Error('加载微信 JS-SDK 失败')) + document.head.appendChild(script) }) } -// 获取微信JS-SDK签名 -const getWxSignature = () => { +function requestWxSignature(apiPath) { + const token = getStoredToken() + if (!token) { + return Promise.reject(new Error('未登录,无法获取微信签名')) + } return new Promise((resolve, reject) => { - // #ifdef H5 - const url = window.location.href.split('#')[0] + const pageUrl = getSignPageUrl() uni.request({ - url: `${ROOTPATH}/api/mobile/user/wechat-share`, + url: `${ROOTPATH}${apiPath}`, method: 'GET', + header: getAuthHeader(), data: { - url: url, + url: pageUrl, activity_tag: 'walksz', activity_list_id: 13 }, success: (res) => { - if (res.data && res.data.signature) { - resolve(res.data) + const parsed = parseWxSignatureResponse(res) + if (parsed) { + resolve(parsed) } else { - reject(new Error('获取签名失败')) + reject(new Error(`签名数据无效: ${apiPath}`)) } }, - fail: (err) => { - reject(err) - } + fail: (err) => reject(err) }) - // #endif - // #ifndef H5 - resolve(null) - // #endif }) } -// 初始化微信JS-SDK -const initWxSDK = (shareConfig = {}) => { - return new Promise((resolve, reject) => { - // #ifdef H5 - loadWxSDK().then(() => { - getWxSignature().then((signatureData) => { - const { signature, timestamp, nonceStr } = signatureData - - wx.config({ +const getWxSignature = async () => { + let lastErr = null + for (let i = 0; i < SIGNATURE_APIS.length; i++) { + try { + return await requestWxSignature(SIGNATURE_APIS[i]) + } catch (e) { + lastErr = e + console.warn('尝试签名接口失败:', SIGNATURE_APIS[i], e.message || e) + } + } + throw lastErr || new Error('获取微信签名失败') +} + +function setupShare(wx, shareConfig = {}) { + const link = shareConfig.link || window.location.href.split('#')[0] + const imgUrl = shareConfig.imgUrl || `${window.location.origin}/h5walksz/static/share.jpg` + wx.updateAppMessageShareData({ + title: shareConfig.title || '打卡苏州市党史教育基地', + desc: shareConfig.desc || '打卡苏州市党史教育基地', + link, + imgUrl + }) + wx.updateTimelineShareData({ + title: shareConfig.title || '打卡苏州市党史教育基地', + link, + imgUrl + }) +} + +/** + * 使用已拿到的签名数据配置 JSSDK(推荐:通过 $u.api.share 获取签名) + */ +export function configWechatJssdk(signatureData, options = {}) { + const parsed = normalizeSignaturePayload(signatureData) + if (!parsed) { + return Promise.reject(new Error('微信签名数据格式不正确')) + } + if (wxInitPromise) return wxInitPromise + + const pageUrl = getSignPageUrl() + if (parsed.signUrl && parsed.signUrl !== pageUrl) { + console.warn('微信签名 URL 与当前页不一致,可能导致 config 失败', { + signUrl: parsed.signUrl, + pageUrl + }) + } + + wxInitPromise = loadWxSDK() + .then(() => { + return new Promise((resolve, reject) => { + wxInstance.config({ debug: false, - appId: WECHAT_APPID, - timestamp: timestamp, - nonceStr: nonceStr, - signature: signature, - jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData'] + appId: parsed.appId || WECHAT_APPID, + timestamp: parsed.timestamp, + nonceStr: parsed.nonceStr, + signature: parsed.signature, + jsApiList: options.jsApiList || parsed.jsApiList || DEFAULT_JS_API }) - - wx.ready(() => { - // 分享到朋友 - wx.updateAppMessageShareData({ - title: shareConfig.title || '打卡苏州市党史教育基地', - desc: shareConfig.desc || '打卡苏州市党史教育基地', - link: shareConfig.link || window.location.href, - imgUrl: shareConfig.imgUrl || `${window.location.origin}/h5walksz/static/share.jpg`, - success: () => { - console.log('分享到朋友成功') - } - }) - - // 分享到朋友圈 - wx.updateTimelineShareData({ - title: shareConfig.title || '打卡苏州市党史教育基地', - link: shareConfig.link || window.location.href, - imgUrl: shareConfig.imgUrl || `${window.location.origin}/h5walksz/static/share.jpg`, - success: () => { - console.log('分享到朋友圈成功') - } - }) - - resolve(wx) + wxInstance.ready(() => { + if (options.setupShare !== false) { + setupShare(wxInstance, options.shareConfig) + } + if (typeof options.onReady === 'function') { + options.onReady(wxInstance) + } + resolve(wxInstance) }) - - wx.error((res) => { - console.error('微信JS-SDK配置失败', res) + wxInstance.error((res) => { + wxInitPromise = null + console.error('wx.config 失败:', res) reject(res) }) - }).catch((err) => { - console.error('获取签名失败', err) - reject(err) }) - }).catch((err) => { - console.error('加载微信JS-SDK失败', err) - reject(err) }) - // #endif - // #ifndef H5 - resolve(null) - // #endif + .catch((err) => { + wxInitPromise = null + throw err + }) + + return wxInitPromise +} + +export function ensureWechatJssdk(options = {}) { + if (!isWechatBrowser()) { + return Promise.reject(new Error('非微信内置浏览器')) + } + if (!getStoredToken()) { + return Promise.reject(new Error('请先登录后再使用微信地图能力')) + } + if (wxInitPromise) return wxInitPromise + return getWxSignature().then((data) => configWechatJssdk(data, options)) +} + +export function getWechatLocation() { + return ensureWechatJssdk({ setupShare: false }).then((wx) => { + return new Promise((resolve, reject) => { + wx.getLocation({ + type: 'gcj02', + success: (res) => { + resolve({ + longitude: res.longitude, + latitude: res.latitude + }) + }, + cancel: () => reject(new Error('用户取消定位')), + fail: (err) => reject(err) + }) + }) + }) +} + +export function openWechatLocation(lat, lng, name, address = '') { + return ensureWechatJssdk({ setupShare: false }).then((wx) => { + return new Promise((resolve, reject) => { + wx.openLocation({ + latitude: parseFloat(lat), + longitude: parseFloat(lng), + name: name || '目的地', + address: address || '', + scale: 16, + success: resolve, + fail: reject + }) + }) }) } +const initWxSDK = (shareConfig = {}) => { + return ensureWechatJssdk({ shareConfig, setupShare: true }) +} + export { loadWxSDK, getWxSignature, initWxSDK } - diff --git a/manifest.json b/manifest.json index 6288d10..10669ac 100644 --- a/manifest.json +++ b/manifest.json @@ -89,11 +89,7 @@ "template" : "", "publicPath" : "/h5walksz/", "sdkConfigs" : { - "maps" : { - "qqmap" : { - "key" : "B4TBZ-G6OLU-NR6VC-GOUUX-6GTHH-BAFUZ" - } - } + "maps" : {} } } } diff --git a/pages/home/home.vue b/pages/home/home.vue index 1086479..280a67e 100644 --- a/pages/home/home.vue +++ b/pages/home/home.vue @@ -25,9 +25,35 @@ + +
+ + + + + + {{pointer.name}} + + + + 距你 {{ formatPointerDistance(pointer.distance) }} + {{ pointerAddress }} + + + + + + 开始前往 + + + + + + - @@ -50,41 +76,6 @@ - - - - - - - - - - {{pointer.name}} - - - - 距你 {{pointer.distance ? pointer.distance + ' 公里' : '-'}} - - - - {{con.value}} - - - - - - - - - 开始前往 - - - - - - - @@ -120,8 +111,8 @@ 使用完整功能,需要获取您的定位,点击授权 - +
c.key === 'address') + return item && item.value ? item.value : '' + } + }, + // #endif + created() { + // #ifdef H5 + this.defaultMarkerUrl = getDefaultMarkerUrl() + // #endif + }, onShareAppMessage() { return shareInfo }, @@ -220,8 +242,9 @@ }, onReady() { this.initLocationAuth() - // 创建地图上下文(H5端和小程序端都支持) + // #ifndef H5 this.mapContext = uni.createMapContext('myMap', this); + // #endif // #ifndef H5 // 获取当前地图层级(小程序端) let _this = this @@ -249,8 +272,16 @@ }, // #ifdef H5 mounted() { - // H5端微信分享已移除,不再初始化 - // this.initWechatShare() + this.defaultMarkerUrl = getDefaultMarkerUrl() + this.$nextTick(() => { + this.initTdtMap() + }) + }, + beforeDestroy() { + if (this.tdtMap) { + this.tdtMap.destroy() + this.tdtMap = null + } }, // #endif onLoad(option) { @@ -300,6 +331,7 @@ console.log('H5端已有token,正常加载数据') // H5端不显示定位提示 this.showLocationTip = false + this.initWechatSdkAfterLogin() this.getArea() this.getConfig() this.initLocationAuth() @@ -340,10 +372,16 @@ } // #endif // #ifdef H5 - // H5端不显示弹窗,直接清除pointer,让用户从列表中查看 + this.pointer = vuex_pointer + if (vuex_latlng && vuex_latlng.lat && vuex_latlng.lng) { + this.pointer.distance = getDistance(vuex_latlng.lat, vuex_latlng.lng, this.pointer.lat, this.pointer.lng) + } + this.showPointer = true + this.scale = 13 uni.removeStorageSync('vuex_pointer') - this.showPointer = false - this.pointer = null + this.$nextTick(() => { + this.centerTdtOnPointer(this.pointer) + }) // #endif } // 答题的点位解锁 @@ -363,24 +401,17 @@ uni.removeStorageSync("vuex_point_id") } }) - // 更新markers中的has_answer - this.markers.map(item => { - if (item.id == vuex_point_id) { - item.has_answer = 1 - } - }) + this.syncTdtMarkers() // #endif } // #ifdef H5 - // 返回页面时,如果markers为空,重新加载点位数据 - if (this.markers.length === 0 && !isNull(this.vuex_token)) { - console.log('onShow: markers为空,重新加载点位数据') + if (this.pointers.length === 0 && !isNull(this.vuex_token)) { this.getPointers() } - // 如果有缓存的定位信息,使用缓存 if (vuex_latlng && vuex_latlng.lat && vuex_latlng.lng) { this.lat = vuex_latlng.lat this.lng = vuex_latlng.lng + this.updateTdtUserLocation() } // #endif }, @@ -396,25 +427,219 @@ }, methods: { // #ifdef H5 + formatPointerDistance(distance) { + if (!distance && distance !== 0) return '-' + if (typeof distance === 'string' && (distance.includes('公里') || distance.includes('米'))) { + return distance + } + return distance + ' 公里' + }, + gcjToTdt(lng, lat) { + return gcj02ToWgs84(lng, lat) + }, + async initTdtMap() { + if (this.tdtMap) return + try { + const defaultCenter = { lng: 120.585316, lat: 31.298886 } + const zoom = uniScaleToTdtZoom(this.scale) + this.tdtMap = new TiandituMapHelper({ + containerId: 'tdtMapContainer', + tk: TIANDITU_TK, + onMarkerClick: (e) => this.showDetail(e) + }) + await this.tdtMap.init(defaultCenter.lng, defaultCenter.lat, zoom) + this.tdtMapReady = true + if (this.lat && this.lng) { + this.updateTdtUserLocation() + } + if (this.tdtMarkersPayload.length) { + await this.$nextTick() + await this.tdtMap.setPointMarkers(this.tdtMarkersPayload) + } + } catch (err) { + console.error('天地图初始化失败:', err) + toast('地图加载失败,请刷新重试') + } + }, + updateTdtUserLocation() { + if (!this.tdtMap || !this.tdtMapReady || !this.lat || !this.lng) return + this.tdtMap.setUserMarker(parseFloat(this.lng), parseFloat(this.lat)) + }, + getMarkerIconUrl(item) { + const url = (item.logo && item.logo.url) || (item.image && item.image.url) + if (url && /^https?:\/\//i.test(url)) { + return url + } + return getDefaultMarkerUrl() + }, + applyLocationSuccess(res) { + this.showLocationTip = false + this.isLoading = false + this.lng = res.longitude + this.lat = res.latitude + uni.setStorageSync('vuex_latlng', { + lng: this.lng, + lat: this.lat, + timestamp: Date.now() + }) + this.updateTdtUserLocation() + this.getPointers() + }, + async initWechatSdkAfterLogin() { + if (!isWechatBrowser()) return + await this.waitForToken(8000) + const token = this.vuex_token || (uni.getStorageSync('walksz_lifeData') || {}).vuex_token + if (!token) { + console.log('未登录,跳过微信 JSSDK 初始化') + return + } + resetWxJssdk() + const pageUrl = getSignPageUrl() + try { + const res = await this.$u.api.share({ url: pageUrl }) + console.log('wechat-share 返回:', res) + const sig = normalizeSignaturePayload(res) + if (!sig) { + throw new Error('share 接口未返回 signature') + } + await configWechatJssdk(sig, { setupShare: true }) + console.log('微信 JSSDK 初始化成功, signUrl:', sig.signUrl, 'pageUrl:', pageUrl) + } catch (e) { + console.log('微信 JSSDK 初始化失败:', e) + } + }, + getUserLocationByWechat() { + return this.initWechatSdkAfterLogin() + .then(() => getWechatLocation()) + .catch((err) => { + console.warn('微信 JSSDK 定位不可用,改用浏览器定位:', err) + return this.getUserLocationByBrowser() + }) + }, + getUserLocationByBrowser() { + return new Promise((resolve, reject) => { + navigator.geolocation.getCurrentPosition( + (pos) => { + const gcj = wgs84ToGcj02(pos.coords.longitude, pos.coords.latitude) + resolve({ + longitude: gcj.lng, + latitude: gcj.lat + }) + }, + (err) => reject(err), + { + enableHighAccuracy: true, + timeout: 10000, + maximumAge: 60000 + } + ) + }) + }, + requestUniGetLocation() { + uni.getLocation({ + type: 'gcj02', + timeout: 10000, + success: (res) => { + console.log('uni.getLocation 成功:', res.latitude, res.longitude) + this._locationFetching = false + this.applyLocationSuccess(res) + }, + fail: (err) => { + console.log('uni.getLocation 失败:', err) + this.tryBrowserLocationAfterFail(err) + } + }) + }, + tryBrowserLocationAfterFail(uniErr) { + const errMsg = (uniErr && uniErr.errMsg) || '' + console.log('定位降级, 原因:', errMsg) + const fallback = () => { + this.getUserLocationByBrowser() + .then((res) => { + console.log('浏览器定位成功:', res.latitude, res.longitude) + this.applyLocationSuccess(res) + }) + .catch((browserErr) => { + console.log('浏览器定位失败:', browserErr) + this.onLocationFinallyFailed() + }) + .finally(() => { + this._locationFetching = false + }) + } + if (isWechatBrowser()) { + this.getUserLocationByWechat() + .then((res) => { + console.log('微信 JSSDK 定位成功:', res.latitude, res.longitude) + this.applyLocationSuccess(res) + this._locationFetching = false + }) + .catch(() => fallback()) + return + } + fallback() + }, + onLocationFinallyFailed() { + this.showLocationTip = false + this.isLoading = false + if (!isNull(this.vuex_token)) { + this.getPointers() + } else { + this.waitForToken(2000).then(() => { + if (!isNull(this.vuex_token)) this.getPointers() + }) + } + }, + async syncTdtMarkers() { + const list = await this.buildTdtMarkerList() + this.tdtMarkersPayload = list + if (!this.tdtMapReady || !this.tdtMap) return + await this.$nextTick() + await this.tdtMap.setPointMarkers(list) + }, + async buildTdtMarkerList() { + const list = [] + const max = Math.min(this.pointers.length, 50) + for (let i = 0; i < max; i++) { + const item = this.pointers[i] + const lat = parseFloat(item.lat) + const lng = parseFloat(item.lng) + if (isNaN(lat) || isNaN(lng)) continue + list.push({ + id: item.id, + lng, + lat, + iconUrl: this.getMarkerIconUrl(item) + }) + } + return list + }, + centerTdtOnPointer(pointer) { + if (!pointer || !this.tdtMap || !this.tdtMapReady) return + const zoom = uniScaleToTdtZoom(this.scale) + this.tdtMap.centerAndZoom(parseFloat(pointer.lng), parseFloat(pointer.lat), zoom) + }, + centerTdtOnUser() { + if (!this.lat || !this.lng || !this.tdtMap || !this.tdtMapReady) return + const zoom = uniScaleToTdtZoom(this.scale) + this.tdtMap.centerAndZoom(parseFloat(this.lng), parseFloat(this.lat), zoom) + this.updateTdtUserLocation() + }, // 创建圆形marker图标(带缓存) createRoundedMarkerIcon(imageUrl) { return new Promise((resolve) => { - // 检查缓存 - if (this.iconCache[imageUrl]) { - resolve(this.iconCache[imageUrl]) + const src = resolveH5AssetUrl(imageUrl) + if (this.iconCache[src]) { + resolve(this.iconCache[src]) return } - - // 如果图片已经是base64或blob,直接返回并缓存 - if (imageUrl.startsWith('data:') || imageUrl.startsWith('blob:')) { - this.iconCache[imageUrl] = imageUrl - resolve(imageUrl) + if (src.startsWith('data:') || src.startsWith('blob:')) { + this.iconCache[src] = src + resolve(src) return } - - // 创建图片对象 const img = new Image() - img.crossOrigin = 'anonymous' // 允许跨域 + img.crossOrigin = 'anonymous' img.onload = () => { try { @@ -438,35 +663,29 @@ // 转换为base64 const base64 = canvas.toDataURL('image/png') - // 缓存结果 - this.iconCache[imageUrl] = base64 + this.iconCache[src] = base64 resolve(base64) } catch (error) { console.error('创建圆形marker图标失败:', error) - // 缓存原图片,避免重复处理 - this.iconCache[imageUrl] = imageUrl - resolve(imageUrl) // 失败时返回原图片 + this.iconCache[src] = src + resolve(src) } } - img.onerror = () => { - console.error('加载marker图片失败:', imageUrl) - // 缓存原图片,避免重复处理 - this.iconCache[imageUrl] = imageUrl - resolve(imageUrl) // 失败时返回原图片 + console.error('加载marker图片失败:', src) + const fallback = this.defaultMarkerUrl || src + this.iconCache[src] = fallback + resolve(fallback) } - - // 设置超时,避免长时间等待 setTimeout(() => { if (!img.complete) { - console.warn('marker图片加载超时:', imageUrl) - // 缓存原图片 - this.iconCache[imageUrl] = imageUrl - resolve(imageUrl) // 超时时返回原图片 + console.warn('marker图片加载超时:', src) + const fallback = this.defaultMarkerUrl || src + this.iconCache[src] = fallback + resolve(fallback) } - }, 2000) // 减少超时时间到2秒 - - img.src = imageUrl + }, 2000) + img.src = src }) }, // #endif @@ -501,14 +720,13 @@ }) // #endif // #ifdef H5 - // H5端直接尝试获取定位,默认不显示提示 + if (this._h5LocationAuthStarted) return + this._h5LocationAuthStarted = true this.showLocationTip = false - // 先显示地图容器,然后尝试获取定位 this.isLoading = true - // 延迟一下再获取定位,确保页面先渲染 - setTimeout(() => { + this.$nextTick(() => { this.getUserLocation() - }, 100) + }) // #endif }, // 我的位置 @@ -544,7 +762,7 @@ this.lng = cachedLocation.lng this.showLocationTip = false this.isLoading = false - // 使用缓存定位后,如果有token则加载点位数据 + this.updateTdtUserLocation() if (!isNull(this.vuex_token)) { this.getPointers() } else { @@ -553,87 +771,66 @@ return } } - // 防抖:如果距离上次定位时间太短,不重复调用 - if (now - this.lastLocationTime < 2000) { - console.log('定位请求过于频繁,跳过本次请求') + if (this._locationFetching) { + console.log('定位请求进行中,跳过重复调用') return } + if (this.lat && this.lng && now - this.lastLocationTime < 5000) { + console.log('已有定位信息,跳过重复定位') + this.updateTdtUserLocation() + if (this.pointers.length === 0) { + this.getPointers() + } + return + } + this._locationFetching = true this.lastLocationTime = now + // 微信公众号 H5:优先微信 JSSDK 定位(gcj02) + if (isWechatBrowser()) { + this.getUserLocationByWechat() + .then((res) => { + console.log('定位成功:', res.latitude, res.longitude) + this.applyLocationSuccess(res) + this._locationFetching = false + }) + .catch((err) => { + console.log('微信/浏览器定位失败,尝试 uni.getLocation:', err) + this.requestUniGetLocation() + }) + return + } + this.getUserLocationByBrowser() + .then((res) => { + this.applyLocationSuccess(res) + this._locationFetching = false + }) + .catch(() => { + this.requestUniGetLocation() + }) + return // #endif uni.getLocation({ type: 'gcj02', - // #ifdef H5 - // H5端增加超时时间 - timeout: 10000, - // #endif success: (res) => { console.log('获取定位成功:', res.latitude, res.longitude) this.showLocationTip = false - // #ifdef H5 - this.isLoading = false - // #endif this.lng = res.longitude this.lat = res.latitude - // 保存定位信息,包含时间戳 uni.setStorageSync('vuex_latlng', { lng: this.lng, lat: this.lat, timestamp: Date.now() }) - // 获取定位后,如果有token则加载点位数据 if (!isNull(this.vuex_token)) { - this.getPointers() - } else { - // #ifdef H5 - // H5端即使没有token,也可以先加载点位数据(不计算距离) this.getPointers() - // #endif } }, fail: (err) => { - console.log("获取定位失败:", err) - // #ifdef H5 - // H5端定位失败不显示提示,直接加载数据 - this.showLocationTip = false - this.isLoading = false - const errMsg = err.errMsg || err.message || '未知错误' - console.log('定位失败原因:', errMsg) - - // H5端定位失败常见原因和处理建议: - if (errMsg.includes('network error')) { - console.warn('定位失败:网络错误,可能原因:') - console.warn('1. 当前页面不是HTTPS协议(定位功能需要HTTPS)') - console.warn('2. 网络连接不稳定') - console.warn('3. 浏览器定位服务被禁用') - // 检查是否是HTTPS - if (window.location.protocol !== 'https:') { - console.error('⚠️ 当前页面不是HTTPS,定位功能无法使用!请使用HTTPS协议访问。') - } - } else if (errMsg.includes('permission') || errMsg.includes('denied')) { - console.warn('定位失败:用户拒绝了定位权限') - } else if (errMsg.includes('timeout')) { - console.warn('定位失败:定位超时') - } - - // 即使定位失败,也尝试加载点位数据(不计算距离) - if (!isNull(this.vuex_token)) { - this.getPointers() - } else { - // 等待token后再加载 - this.waitForToken(2000).then(() => { - if (!isNull(this.vuex_token)) { - this.getPointers() - } - }) - } - // #endif - // #ifndef H5 + console.log('获取定位失败:', err) this.showLocationTip = true - // 即使定位失败,如果有token也可以加载点位数据(不计算距离) if (!isNull(this.vuex_token)) { this.getPointers() } - // #endif } }) }, @@ -763,6 +960,7 @@ window.history.replaceState({}, '', newUrl) // token设置完成后,再加载需要token的数据 console.log('开始加载需要token的数据') + this.initWechatSdkAfterLogin() this.getArea() this.getConfig() this.initLocationAuth() @@ -1015,19 +1213,8 @@ } // #endif // #ifdef H5 - // H5端移动地图到当前位置 - if (this.mapContext && this.lat && this.lng) { - this.mapContext.moveToLocation({ - longitude: this.lng, - latitude: this.lat - }); - } else if (this.lat && this.lng) { - // 如果mapContext不存在,直接设置scale和center - this.scale = 13 - console.log('H5端移动到当前位置:', this.lat, this.lng) - } else { - console.warn('H5端没有定位信息,无法移动到当前位置') - } + this.scale = 13 + this.centerTdtOnUser() // #endif }, async getPointers() { @@ -1051,188 +1238,59 @@ this.isLoading = true // #endif this.pointers = [] - this.markers = [] // H5端也需要生成markers + this.markers = [] try { const res = await this.$u.api.getPoints({ theme_id: this.theme_id, }) - console.log('获取点位数据成功,完整响应:', res) - console.log('点位数量:', res.points ? res.points.length : 0) this.pointers = res.points || [] - console.log('pointers数组:', this.pointers) - if (this.pointers.length === 0) { - console.warn('点位数据为空,请检查接口返回') - } - // #ifdef H5 - // H5端先同步生成markers,然后异步更新圆形图标 - const markerPromises = [] - // #endif - this.pointers.map((item, index) => { + this.pointers.forEach((item) => { if (this.lat && this.lng) { - item.distance = getDistance(this.lat, this.lng, item.lat, item.lng) + item.distance = getDistance(this.lat, this.lng, item.lat, item.lng) } - // H5端和小程序端都需要生成markers - if (index < 50) { // H5端可以显示更多点位 - // #ifdef H5 - // H5端marker格式(使用标准格式) - const truePath = item.logo ? item.logo.url : (item.image ? item.image.url : '/static/share.jpg') - // H5端iconPath可以使用网络图片URL - // 优先使用truePath(网络图片),如果没有则使用本地图片 - let iconPath = truePath && truePath.startsWith('http') ? truePath : '/static/home-marker.png' - // 如果是本地路径,转换为完整URL - if (iconPath.startsWith('/') && !iconPath.startsWith('http')) { - iconPath = window.location.origin + '/h5walksz' + iconPath - } - // 先检查是否有圆形图标缓存,如果有就直接使用 - const cachedRoundedIcon = this.iconCache && this.iconCache[iconPath] - // 先使用原图片创建marker,后续异步更新为圆形图片 - const marker = { + }) + // #ifndef H5 + this.pointers.forEach((item, index) => { + if (index < 10) { + this.markers.push({ id: item.id, latitude: parseFloat(item.lat), longitude: parseFloat(item.lng), - iconPath: cachedRoundedIcon || iconPath, // 如果有缓存就用圆形图标,否则用原图片 - originalIconPath: iconPath, // 保存原始路径,用于后续处理 - width: 40, // 增加marker尺寸,使图片更清晰 - height: 40, + iconPath: '/static/home-marker.png', + truePath: item.logo ? item.logo.url : (item.image ? item.image.url : '/static/share.jpg'), + width: 0, + height: 0, title: item.name, - truePath: truePath, // 保存图片路径,用于显示 - has_answer: item.has_answer, // 保存答题状态 - callout: { - content: item.name, - color: '#333', - fontSize: 12, - borderRadius: 8, - bgColor: '#fff', - padding: 8, - display: 'BYCLICK', - textAlign: 'center', - borderWidth: 1, - borderColor: '#e5e5e5' - } - } - this.markers.push(marker) - // 如果已经有缓存,就不需要再处理了 - if (!cachedRoundedIcon) { - // 异步处理圆形图标 - markerPromises.push( - this.createRoundedMarkerIcon(iconPath).then(roundedIconPath => { - if (!roundedIconPath || roundedIconPath === iconPath) return - const markerIndex = this.markers.findIndex(m => m.id === marker.id) - if (markerIndex !== -1) { - // 检查marker是否还存在(可能被重新生成了) - const currentMarker = this.markers[markerIndex] - if (currentMarker && currentMarker.id === marker.id) { - // 使用Vue.set确保响应式更新,并保留所有属性 - const updatedMarker = { - ...currentMarker, - iconPath: roundedIconPath - } - this.$set(this.markers, markerIndex, updatedMarker) - console.log('H5端更新marker圆形图标:', marker.id, 'iconPath长度:', roundedIconPath.length) - } else { - console.warn('H5端marker已被更新,跳过圆形图标更新:', marker.id) - } - } - }).catch(err => { - console.warn('处理marker图标失败:', err) - }) - ) - } - console.log('H5端生成marker:', marker.id, 'iconPath:', marker.iconPath, 'truePath:', marker.truePath) - // #endif - // #ifndef H5 - // 小程序端marker格式 - const marker = { - id: item.id, - latitude: parseFloat(item.lat), - longitude: parseFloat(item.lng), - iconPath: '/static/home-marker.png', - truePath: item.logo ? item.logo.url : (item.image?item.image.url:'/static/share.jpg'), - width: 0, - height: 0, - title: item.name, - distance: item.distance ? parseInt(item.distance) : 0, - has_answer: item.has_answer, - customCallout: { - anchorX: 70, - anchorY: 30, - display: 'ALWAYS' - } - } - // #endif - this.markers.push(marker) + distance: item.distance ? parseInt(item.distance) : 0, + has_answer: item.has_answer, + customCallout: { + anchorX: 70, + anchorY: 30, + display: 'ALWAYS' + }, + }) } }) - // #ifdef H5 - // H5端等待所有圆形图标处理完成(不阻塞主流程) - if (markerPromises.length > 0) { - Promise.all(markerPromises).then(() => { - console.log('H5端所有marker圆形图标处理完成') - // 不强制更新,让每个marker的更新自己触发 - }).catch(err => { - console.warn('H5端处理marker图标时出错:', err) - }) - } // #endif - console.log('生成的markers数量:', this.markers.length) - console.log('markers数据:', JSON.stringify(this.markers.slice(0, 3))) // 打印前3个marker用于调试 // #ifdef H5 - console.log('H5端点位列表渲染,pointers长度:', this.pointers.length, 'markers长度:', this.markers.length) - // 确保地图有中心点和缩放级别 if (!this.lat || !this.lng) { - // 如果没有定位,使用第一个点位作为地图中心 - if (this.markers.length > 0) { - this.lat = this.markers[0].latitude - this.lng = this.markers[0].longitude - console.log('H5端使用第一个点位作为地图中心:', this.lat, this.lng) + if (this.pointers.length > 0) { + this.lat = parseFloat(this.pointers[0].lat) + this.lng = parseFloat(this.pointers[0].lng) } } - // 确保地图有缩放级别 if (!this.scale || this.scale < 10) { this.scale = 13 - console.log('H5端设置地图缩放级别:', this.scale) } - // 强制更新markers(H5端可能需要等待地图加载完成) - this.$nextTick(() => { - console.log('H5端地图更新markers,当前markers数量:', this.markers.length) - // 延迟一下,确保地图已经渲染 - setTimeout(() => { - // 强制触发Vue的响应式更新(保留圆形图标) - // 深拷贝markers,确保保留所有属性包括圆形图标和callout - const markers = this.markers.map(m => { - // 检查是否有圆形图标缓存,如果有就使用圆形图标 - const originalPath = m.originalIconPath || m.iconPath - const cachedRoundedIcon = this.iconCache && this.iconCache[originalPath] - const newMarker = { - ...m, - // 优先使用圆形图标缓存,其次使用当前的iconPath - iconPath: cachedRoundedIcon || m.iconPath, - } - if (m.callout) { - newMarker.callout = {...m.callout} - } - return newMarker - }) - this.markers = [] - this.$nextTick(() => { - this.markers = markers - console.log('H5端强制更新markers完成,数量:', this.markers.length, '第一个iconPath类型:', typeof this.markers[0]?.iconPath) - // 移动地图到第一个marker位置,触发地图更新 - if (this.mapContext && this.markers.length > 0) { - this.mapContext.moveToLocation({ - longitude: this.markers[0].longitude, - latitude: this.markers[0].latitude, - success: () => { - console.log('H5端地图移动到第一个marker位置成功') - }, - fail: (err) => { - console.error('H5端地图移动失败:', err) - } - }) + if (!this.tdtMapReady) { + await this.initTdtMap() + } + await this.syncTdtMarkers() + if (this.pointers.length > 0) { + this.centerTdtOnPointer(this.pointers[0]) + } else if (this.lat && this.lng) { + this.centerTdtOnUser() } - }) - }, 500) - }) this.isLoading = false // #endif } catch (err) { @@ -1295,58 +1353,79 @@ }, closeDetail() { this.showPointer = false - setTimeout(function() { + setTimeout(() => { this.pointer = null - }, 100); - + }, 100) }, showDetail(e) { - // 获取markerId,兼容小程序和H5 const markerId = e.markerId || e.detail?.markerId || e.detail?.marker?.id - console.log('showDetail触发,markerId:', markerId, 'event:', e) const arr = this.pointers.filter(item => item.id == markerId) if (arr.length > 0) { this.pointer = arr[0] if (this.lat && this.lng) { - this.pointer.distance = getDistance(this.lat, this.lng, this.pointer.lat, this.pointer.lng) - } - this.showPointer = true + this.pointer.distance = getDistance(this.lat, this.lng, this.pointer.lat, this.pointer.lng) + } + this.showPointer = true + this.showNear = true + // #ifdef H5 + this.centerTdtOnPointer(this.pointer) + // #endif } }, - toMap(e) { + async toMap(e) { if (!this.pointer) return // #ifndef H5 toMapAPP(this.pointer.lat, this.pointer.lng, this.pointer.name) // #endif // #ifdef H5 - // H5端打开腾讯地图URL - const address = this.pointer.config && this.pointer.config.length > 0 ? - this.pointer.config.find(c => c.key === 'address')?.value || '' : '' - const url = this.getMapUrl(this.pointer.lat, this.pointer.lng, this.pointer.name, address) + if (isWechatBrowser()) { + try { + await this.initWechatSdkAfterLogin() + await openWechatLocation( + this.pointer.lat, + this.pointer.lng, + this.pointer.name, + this.pointerAddress + ) + } catch (err) { + console.log('微信打开导航失败:', err) + toast('打开导航失败,请稍后重试') + } + return + } + const url = this.getMapUrl(this.pointer.lat, this.pointer.lng, this.pointer.name) window.open(url, '_blank') // #endif }, // #ifdef H5 - // H5端:选择点位 selectPointer(item) { this.pointer = item - this.pointer.distance = getDistance(this.lat, this.lng, this.pointer.lat, this.pointer.lng) + if (this.lat && this.lng) { + this.pointer.distance = getDistance(this.lat, this.lng, this.pointer.lat, this.pointer.lng) + } this.showPointer = true + this.centerTdtOnPointer(item) }, - // H5端:打开腾讯地图URL - openMapUrl(item) { - const address = item.config && item.config.length > 0 ? - item.config.find(c => c.key === 'address')?.value || '' : '' - const url = this.getMapUrl(item.lat, item.lng, item.name, address) + async openMapUrl(item) { + if (isWechatBrowser()) { + const addrItem = item.config && item.config.find(c => c.key === 'address') + try { + await openWechatLocation(item.lat, item.lng, item.name, addrItem ? addrItem.value : '') + } catch (err) { + toast('打开导航失败') + } + return + } + const url = this.getMapUrl(item.lat, item.lng, item.name) window.open(url, '_blank') }, - // H5端:生成地图URL - getMapUrl(lat, lng, name, address) { - if (lat && lng) { - const marker = `coord:${lat},${lng};title:${encodeURIComponent(name || '位置')};addr:${encodeURIComponent(address || '')}` - return `https://apis.map.qq.com/uri/v1/marker?marker=${marker}&referer=myapp` + getMapUrl(gcjLat, gcjLng, name) { + if (gcjLat && gcjLng) { + const wgs = this.gcjToTdt(gcjLng, gcjLat) + return buildTiandituNavUrl(wgs.lng, wgs.lat, name) } - return 'https://apis.map.qq.com/uri/v1/marker?marker=coord:31.297241,120.580792;title:行走红色苏州;addr:苏州市姑苏区&referer=myapp' + const def = this.gcjToTdt(120.585316, 31.298886) + return buildTiandituNavUrl(def.lng, def.lat, '行走红色苏州') }, // H5端:初始化微信分享(已移除,不再调用接口) // initWechatShare() { @@ -1444,19 +1523,66 @@ #myMap { width: 100%; height: 100vh; - // #ifdef H5 - // H5端确保地图容器有明确的尺寸 + } + + // #ifdef H5 + .maps-tdt { + width: 100%; + height: 100vh; min-height: 500px; position: relative; - - // 尝试通过CSS为marker图标添加圆角(如果map组件支持) - ::v-deep .marker-icon, - ::v-deep .marker-icon { - border-radius: 100% !important; - overflow: hidden; + z-index: 1; + } + + .maps-info-near--h5 { + position: fixed; + right: 12px; + bottom: calc(78px + env(safe-area-inset-bottom, 0px)); + z-index: 500; + pointer-events: auto; + + image { + width: 123px; + height: 66px; + display: block; } - // #endif } + + .maps-info-pointer--h5 { + position: fixed; + left: 0; + right: 0; + bottom: calc(68px + env(safe-area-inset-bottom, 0px)); + z-index: 501; + pointer-events: auto; + max-height: 42vh; + overflow-y: auto; + background: #fff; + border-radius: 16px 16px 0 0; + box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.12); + } + + /* 仅约束标注图标尺寸,勿用 width:auto 否则会显示原图大小 */ + .maps-tdt ::v-deep .tdt-marker-pane { + z-index: 600 !important; + } + + .maps-tdt ::v-deep img.tdt-marker-icon { + width: 44px !important; + height: 44px !important; + max-width: 44px !important; + max-height: 44px !important; + min-width: 0 !important; + min-height: 0 !important; + object-fit: cover !important; + border-radius: 50%; + border: 2px solid #fff; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25); + box-sizing: border-box; + opacity: 1 !important; + visibility: visible !important; + } + // #endif // #ifdef H5 &-h5 { @@ -1580,27 +1706,15 @@ &-near { position: fixed; right: 30rpx; - // #ifndef H5 - bottom: 200rpx; // 在tabbar上方 - // #endif - // #ifdef H5 - bottom: 100px; // H5端使用px单位,在tabbar上方 - // #endif - z-index: 9999; // 提高层级,确保在地图之上 - cursor: pointer; // H5端添加鼠标指针样式 - transition: bottom 0.3s ease-in-out; // 添加过渡动画 - + bottom: 200rpx; + z-index: 99; + cursor: pointer; + image { width: 246rpx; height: 132rpx; - pointer-events: auto; // 确保可以点击 } - - } - &.maps-info-near-up { - bottom: 420rpx !important; - } } // #endif @@ -1610,13 +1724,6 @@ bottom: 160rpx; width: 100%; z-index: 99; - - // #ifdef H5 - position: fixed; - bottom: 180rpx; // H5端为tabbar留出空间 - z-index: 9999; // H5端提高层级,确保在地图之上 - pointer-events: none; // 容器不拦截事件,让子元素可以点击 - // #endif &-pointer { background: #fff; @@ -1627,9 +1734,8 @@ font-size: 28rpx; box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1); // #ifdef H5 - pointer-events: auto; // H5端确保可以点击 - border-radius: 20px 20px 0 0; - box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); + border-radius: 16px 16px 0 0; + box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.12); // #endif &-header {