// common-page/pages/web-view/index.js const Parse = getApp().Parse; const company = getApp().globalData.company; const DEBUG_WEBVIEW = false; const debugLog = (...args) => { if (DEBUG_WEBVIEW) { console.log(...args); } }; Page({ /** * 页面的初始数据 */ data: { path: "", currentTitle: "", // 当前标题 miniPayVisible: false, miniPayPrice: 0, miniPayTradeNo: "", miniPayOrderId: "", miniPayOrderType: "shopgoods", miniPayShowType: "all", miniPayShowBonus: false, miniPayProfileId: "", miniPayProcessing: false, miniPayRequestContext: null, }, // 标题轮询定时器 titlePollingTimer: null, /** * 生命周期函数--监听页面加载 */ onLoad: async function (options) { debugLog('======= web-view 页面加载 ======='); // 1. 先检查用户登录状态 const loginCheck = await this.checkUserLogin(options); if (!loginCheck) { debugLog('⚠️ 用户未登录或没有手机号,已跳转到授权页面'); return; // 停止后续加载 } debugLog('✅ 用户已登录且有手机号,继续加载 web-view'); // 2. 解码 URL let path = decodeURIComponent(options.path || ''); const isSkipAuth = options.skipAuth === 'true'; const isHomePath = /^https?:\/\/www\.yuban\.co\/home([/?#]|$)/.test(path); // 引导页直达 H5 首页时走快速路径: // 仅设置 web-view URL,避免额外参数拼接和 Parse 查询造成首开延迟。 if (isSkipAuth && isHomePath) { this.setData({ path }); return; } debugLog('原始 options.path:', options.path); debugLog('解码后的 path:', path); // 拼接额外参数(避免重复添加已存在的参数) let hasQuery = path.indexOf('?') !== -1; let parsm = hasQuery ? '&' : '?'; let params = []; // 提取 path 中已有的参数 let existingParams = new Set(); if (hasQuery) { const queryString = path.split('?')[1]; if (queryString) { queryString.split('&').forEach(param => { const key = param.split('=')[0]; existingParams.add(key); }); } } // 只添加 path 中不存在的参数 for (const key in options) { if(key != 'path' && key != 'url' && !existingParams.has(key)){ params.push(key + '=' + options[key]); } } // 添加用户信息,确保 H5 能获取到当前用户身份(用于扣减流量等操作) const currentUser = Parse.User.current(); if (currentUser) { // 添加 userId if (!existingParams.has('userId')) { params.push('userId=' + currentUser.id); } // 添加 token (sessionToken) const sessionToken = currentUser.getSessionToken(); if (sessionToken && !existingParams.has('token')) { params.push('token=' + sessionToken); } // 添加 mobile const mobile = currentUser.get('mobile'); if (mobile && !existingParams.has('mobile')) { params.push('mobile=' + mobile); } } if(params.length > 0) { parsm = parsm + params.join('&'); path = path + parsm; } debugLog('最终 web-view URL:', path); debugLog('URL 长度:', path.length); this.setData({ path: path }) // 立即设置标题 const passedStoreName = options.storeName ? decodeURIComponent(options.storeName) : ''; const passedStoreId = options.storeId || ''; // /home 场景下默认不需要门店标题:跳过 ShopStore 的 Parse 查询以提速。 // 这里根据 H5 path 判断:例如 https://www.yuban.co/home?token=...&skipAuth=true... const isHome = isHomePath; if (passedStoreName) { this.setNavigationTitle(passedStoreName); } // 异步加载完整店铺信息(作为备份) // 仅当不是 /home 或者传了门店参数时才查询。 if (!(isHome && !passedStoreId && !passedStoreName)) { this.loadAndSetStoreTitle(passedStoreId, passedStoreName); } // 启动标题轮询监听 this.startTitlePolling(); }, /** * 检查用户登录状态 * @returns {Promise} true: 已登录且有手机号,false: 需要登录 */ checkUserLogin: async function(options) { try { debugLog('🔍 开始检查用户登录状态...'); // 检查是否有 skipAuth 参数(用于跳过登录检查) if (options.skipAuth === 'true') { debugLog('ℹ️ 检测到 skipAuth 参数,跳过登录检查'); return true; } // 检查是否是扫码进入 const isScanEntry = options && (options.storeId && (options.scanCount || options.partnerId || options.userId || options.employeeId)); if (isScanEntry) { debugLog('🚀 检测到扫码进入,准备强制登录以确保流量扣减'); } // 1. 先调用 checkAuth 初始化用户(扫码进入时强制授权) debugLog('📱 调用 checkAuth 初始化用户...'); try { // 如果是扫码进入,则强制授权;否则不强制(由后面逻辑决定) await getApp().checkAuth(isScanEntry); debugLog('✅ checkAuth 调用成功'); } catch (err) { console.warn('⚠️ checkAuth 调用失败:', err); } // 2. 检查用户状态 const currentUser = Parse.User.current(); const hasMobile = currentUser?.get('mobile'); const userLogin = wx.getStorageSync('userLogin'); debugLog('📊 用户状态:'); debugLog(' - 当前用户:', currentUser ? currentUser.id : '无'); debugLog(' - 手机号:', hasMobile || '无'); debugLog(' - userLogin 存储:', userLogin || '无'); // 只有同时满足以下条件才认为已完整登录: // 1. Parse.User.current() 存在 // 2. 用户有手机号 // 3. userLogin 存储存在 if (currentUser && hasMobile && userLogin) { debugLog('✅ 用户已完整登录'); return true; } // 用户未登录或没有手机号,跳转到授权页面 debugLog('⚠️ 用户未完整登录,准备跳转到授权页面'); // 构建返回 URL(登录成功后返回当前页面) const currentPath = options.path || ''; const returnUrl = encodeURIComponent(currentPath); debugLog('🔗 returnUrl:', returnUrl); // 跳转到授权页面,并传递 returnUrl wx.redirectTo({ url: `/components/app-auth/index?returnUrl=${returnUrl}`, success: () => { debugLog('✅ redirectTo 到授权页面成功'); }, fail: (err) => { console.error('❌ redirectTo 失败:', err); // 降级:使用 navigateTo wx.navigateTo({ url: `/components/app-auth/index?returnUrl=${returnUrl}`, success: () => { debugLog('✅ navigateTo 到授权页面成功'); }, fail: (err2) => { console.error('❌ navigateTo 也失败:', err2); // 最后降级:使用 reLaunch wx.reLaunch({ url: `/components/app-auth/index?returnUrl=${returnUrl}`, success: () => { debugLog('✅ reLaunch 到授权页面成功'); }, fail: (err3) => { console.error('❌ 所有跳转方式都失败:', err3); } }); } }); } }); return false; } catch (err) { console.error('❌ 检查登录状态失败:', err); debugLog('checkUserLogin error'); // 出错时也跳转到授权页面 const returnUrl = options.path ? encodeURIComponent(options.path) : ''; wx.redirectTo({ url: `/components/app-auth/index?returnUrl=${returnUrl}`, fail: () => { wx.navigateTo({ url: `/components/app-auth/index?returnUrl=${returnUrl}` }); } }); return false; } }, onReady: function () { }, onShow: function () { this.startTitlePolling(); }, onHide: function () { this.stopTitlePolling(); }, onUnload: function () { this.stopTitlePolling(); }, /** * 页面相关事件处理函数--监听用户下拉动作 */ onPullDownRefresh: function () { }, /** * 页面上拉触底事件的处理函数 */ onReachBottom: function () { }, /** * 用户点击右上角分享 */ onShareAppMessage: function () { }, /** * 处理来自 H5 页面的消息 */ handleMessage: function (e) { try { const messages = e.detail.data || []; // 找到最后一个标题更新消息 let lastTitleMessage = null; let lastPayMessage = null; for (let i = messages.length - 1; i >= 0; i--) { const msg = messages[i]; if (msg.type === 'updateTitle' && msg.title) { lastTitleMessage = msg; } if (!lastPayMessage && msg.type === 'requestMiniPay') { lastPayMessage = msg; } if (lastTitleMessage && lastPayMessage) { break; } } // 更新标题 if (lastTitleMessage) { this.setNavigationTitle(lastTitleMessage.title); } if (lastPayMessage) { this.handleMiniPayRequest(lastPayMessage.payload || {}); } } catch (error) { console.error('❌ 处理消息失败:', error); } }, normalizeMiniPayPayload(payload = {}) { const price = Number(payload.price); return { tradeNo: payload.tradeNo || '', price: Number.isFinite(price) ? price : 0, orderId: payload.orderId || '', orderType: payload.orderType || 'shopgoods', showType: payload.showType || 'all', showBonus: !!payload.showBonus, profileId: payload.profileId || '', scene: payload.scene || '', bizId: payload.bizId || '', callbackUrl: payload.callbackUrl || '' }; }, handleMiniPayRequest(payload = {}) { const parsed = this.normalizeMiniPayPayload(payload); if (!parsed.tradeNo) { wx.showToast({ title: '支付参数缺少tradeNo', icon: 'none' }); return; } if (parsed.price < 0) { wx.showToast({ title: '支付金额无效', icon: 'none' }); return; } if (this.data.miniPayProcessing) { wx.showToast({ title: '支付进行中,请稍后', icon: 'none' }); return; } this.setData({ miniPayVisible: false, miniPayPrice: parsed.price, miniPayTradeNo: parsed.tradeNo, miniPayOrderId: parsed.orderId, miniPayOrderType: parsed.orderType, miniPayShowType: parsed.showType, miniPayShowBonus: parsed.showBonus, miniPayProfileId: parsed.profileId, miniPayProcessing: true, miniPayRequestContext: parsed }, () => { const paymentComp = this.selectComponent('#miniPayComponent'); if (!paymentComp || typeof paymentComp.pay !== 'function') { this.setData({ miniPayProcessing: false }); wx.showToast({ title: '支付组件未就绪', icon: 'none' }); return; } paymentComp.pay(); }); }, onMiniPayResult(e) { const detail = e.detail || {}; const payState = detail.params; const result = { type: 'miniPayResult', payload: { success: payState === 'ok', tradeNo: detail.no || this.data.miniPayTradeNo, payType: detail.type || 'wxpay', cancelled: payState === 'Cancel the payment', errMsg: payState && payState !== 'ok' && payState !== 'Cancel the payment' ? (typeof payState === 'string' ? payState : (payState.errMsg || '')) : '' } }; this.setData({ miniPayVisible: false, miniPayProcessing: false }); this.notifyH5MiniPayResult(result); }, notifyH5MiniPayResult(result) { const ctx = this.data.miniPayRequestContext || {}; console.log('miniPayResult:', result); // web-view 不支持直接由小程序向 H5 反向 postMessage, // 若 H5 传入 callbackUrl,则通过刷新 web-view URL 回传支付结果。 if (!ctx.callbackUrl) return; try { const separator = ctx.callbackUrl.includes('?') ? '&' : '?'; const query = [ 'miniPayResult=1', `success=${result.payload.success ? 1 : 0}`, `tradeNo=${encodeURIComponent(result.payload.tradeNo || '')}`, `payType=${encodeURIComponent(result.payload.payType || '')}`, `cancelled=${result.payload.cancelled ? 1 : 0}`, `errMsg=${encodeURIComponent(result.payload.errMsg || '')}` ].join('&'); const callbackPath = `${ctx.callbackUrl}${separator}${query}`; this.setData({ path: callbackPath }); } catch (error) { console.error('❌ 回传支付结果失败:', error); } }, /** * 设置导航栏标题(统一方法) */ setNavigationTitle: function (title) { // 当前页面使用 custom 导航,仅保留系统胶囊,不更新标题栏。 return; if (!title) { return; } // 若与当前标题一致则跳过,避免频繁触发 if (title === this.data.currentTitle) { return; } // 简单节流:500ms 内重复更新跳过 if (!this._lastTitleUpdateTs) { this._lastTitleUpdateTs = 0; } const now = Date.now(); if (now - this._lastTitleUpdateTs < 500) { return; } this._lastTitleUpdateTs = now; // 更新当前标题记录 this.setData({ currentTitle: title }); // 延迟调用微信 API 设置标题,确保页面已准备好 setTimeout(() => { wx.setNavigationBarTitle({ title: title, success: () => { console.log('✅ web-view 标题设置成功:', title); }, fail: (err) => { console.warn('⚠️ web-view 标题设置失败(可忽略):', err.errMsg); // 不影响主流程,静默失败 } }); }, 100); }, startTitlePolling: function () { this.stopTitlePolling(); }, stopTitlePolling: function () { if (this.titlePollingTimer) { clearInterval(this.titlePollingTimer); this.titlePollingTimer = null; } }, /** * 加载店铺信息并设置页面标题 */ loadAndSetStoreTitle: async function (storeId = '', storeName = '') { try { let finalTitle = storeName; if (!finalTitle) { // 如果没有传入名字,按传入的 storeId 精确查询;再不行按 company 兜底 if (storeId) { const q = new Parse.Query('ShopStore'); const s = await q.get(storeId); if (s) { // 优先使用门店地址,如果没有地址则使用门店名称 const address = s.get('address'); const name = s.get('storeName'); finalTitle = address || name || ''; console.log('📍 web-view 门店信息:', { id: storeId, name: name, address: address, displayTitle: finalTitle }); } } if (!finalTitle) { const storeQuery = new Parse.Query('ShopStore'); storeQuery.equalTo('company', company); storeQuery.ascending('score'); storeQuery.limit(1); const store = await storeQuery.first(); if (store) { // 优先使用门店地址,如果没有地址则使用门店名称 const address = store.get('address'); const name = store.get('storeName'); finalTitle = address || name || ''; } } } if (!finalTitle) return; // 使用统一的设置标题方法 this.setNavigationTitle(finalTitle); } catch (e) { console.error('设置 web-view 标题失败:', e); } } })