/* * 微信公众号 H5 - 微信 JS-SDK * 签名接口 /wechat-share 需要 Bearer token,须在登录成功后调用 */ import { ROOTPATH, WECHAT_APPID } from '@/common/config.js' 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) } const loadWxSDK = () => { return new Promise((resolve, reject) => { if (typeof window === 'undefined') { reject(new Error('非浏览器环境')) return } 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) }) } function requestWxSignature(apiPath) { const token = getStoredToken() if (!token) { return Promise.reject(new Error('未登录,无法获取微信签名')) } return new Promise((resolve, reject) => { const pageUrl = getSignPageUrl() uni.request({ url: `${ROOTPATH}${apiPath}`, method: 'GET', header: getAuthHeader(), data: { url: pageUrl, activity_tag: 'walksz', activity_list_id: 13 }, success: (res) => { const parsed = parseWxSignatureResponse(res) if (parsed) { resolve(parsed) } else { reject(new Error(`签名数据无效: ${apiPath}`)) } }, fail: (err) => reject(err) }) }) } 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: parsed.appId || WECHAT_APPID, timestamp: parsed.timestamp, nonceStr: parsed.nonceStr, signature: parsed.signature, jsApiList: options.jsApiList || parsed.jsApiList || DEFAULT_JS_API }) wxInstance.ready(() => { if (options.setupShare !== false) { setupShare(wxInstance, options.shareConfig) } if (typeof options.onReady === 'function') { options.onReady(wxInstance) } resolve(wxInstance) }) wxInstance.error((res) => { wxInitPromise = null console.error('wx.config 失败:', res) reject(res) }) }) }) .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 }