| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540 |
- // 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<boolean>} 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);
- }
- }
- })
|