main
lion 3 weeks ago
parent 0d2267363a
commit 490a6a6fd6

@ -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 }

@ -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))
}

@ -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)
}

@ -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}`
}

@ -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

@ -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
}

@ -89,11 +89,7 @@
"template" : "",
"publicPath" : "/h5walksz/",
"sdkConfigs" : {
"maps" : {
"qqmap" : {
"key" : "B4TBZ-G6OLU-NR6VC-GOUUX-6GTHH-BAFUZ"
}
}
"maps" : {}
}
}
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save