Files
lucky_shop/pages/index/public/js/index.js
ZF sun ff0d26908c fix(member): 修复账号注销/退出登录后无法正常跳转到微信原生授权登录的问题
修复背景:
  账号注销成功后, 用户被重定向到 native 登录页 (手机号+验证码表单风格, 像 H5),
  而不是微信小程序原生授权登录 (ns-login 弹窗 + getPhoneNumber 授权).
  此外, 微信小程序下短时间内连发 reLaunch 会触发 native 错误:
    appLaunch with non-empty page stack
  以及历史遗留的 navigateTo push 登录页 + 旧页面在栈中导致的死循环问题.

主要修复:

1. util.js redirectTo reLaunch 分支 (4 平台兼容):
   - H5 平台: window.location.replace (真正清空 history, 避免 history.back + router.replace 假清栈)
   - 小程序平台 (mp-weixin/mp-harmony/...): 加 300ms 全局跳转锁 + setTimeout(0) 包裹
     解决 appLaunch with non-empty page stack 错误
   - APP/其他: 保留原生 uni.reLaunch

2. util.js redirectTo 入口智能默认 (跨平台兜底):
   - 目标 == LOGIN_PAGE_URL && mode 是默认 navigateTo && 没传 back 参数
     → 自动改成 reLaunch (避免注销后死循环)
   - 显式传 mode 或带 back 参数的场景保持原行为 (不影响登录后回原页面)

3. member-public.js mixin 增强:
   - getLoginEntryUrl(): 跨平台登录入口智能分流
     微信小程序/鸿蒙/其他小程序 → INDEX_PAGE_URL (首页, 走 ns-login 原生授权弹窗)
     H5/快应用/APP → LOGIN_PAGE_URL (原生登录页)
   - redirectToLoginOnce(): 统一封装 clearLoginState + 跳登录入口, 带 800ms 组件级防抖锁
   - logout / getCancelStatus 兜底 / submitCancelApply 三个地方都改用 redirectToLoginOnce()

4. 5 个注销/退出流程页面改造:
   - cancelsuccess.vue / cancelstatus.vue / cancellation.vue / cancelrefuse.vue
     / assets.vue: 跳登录入口全部用 redirectToLoginOnce (不再直接 redirectTo(LOGIN_PAGE_URL))
   - cancellation.vue / cancelrefuse.vue: 加 memberPublic mixin (原本没有)

5. 取消了在 pages/index/public/js/index.js 中 onShow 自动弹登录弹窗的尝试
   (因触发 Vue 框架的 dataset of null pre-existing 错误, 改由其他途径处理)

测试覆盖 (300+ 测试用例全部通过):
  - tests/member-cancel-flow.test.js (28 个测试, 9 个用户故事场景)
  - tests/redirectTo-relaunch-h5.test.js (11 个测试, 9 个跨平台场景)
  - tests/redirectTo-real.test.js (12 个)
  - tests/util.test.js (30 个)
  - 其他 8 个测试文件全部通过
2026-07-23 17:26:00 +08:00

172 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import splashState from '@/common/js/splash-state.js';
import Config from '@/common/js/config.js';
export default {
data() {
return {
minScrollTop: 100, // 设置回到顶端按钮显示要求,最小页面滚动距离
wechatQrcode: '', // 公众号二维码
diyRoute: '/pages/index/index',
shopInfo: null, // 店铺信息
diyGroupReady: false // diy-group 内容是否已渲染完毕(用于控制版本水印显示时机)
};
},
computed: {
versionWatermark() {
let v = null;
try {
v = typeof __app_version !== 'undefined' ? __app_version : null;
} catch (e) {}
if (!v || !v.commitId || !v.buildTime) return '';
const date = String(v.buildTime).slice(0, 16).replace('T', ' ');
const commit = String(v.commitId);
return `Ver.${date} · ${commit}`;
}
},
// 鸿蒙元服务环境: 系统 TabBar 点击 uni.switchTab 静默失败
// 手动接管 onTabItemTap, 用 uni.reLaunch 兜底, 确保 TabBar 跳转在鸿蒙下也能工作
// #ifdef MP-HARMONY
onTabItemTap(e) {
if (!e || !e.pagePath) return;
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const currentPath = currentPage ? '/' + currentPage.route : '';
const targetPath = e.pagePath.startsWith('/') ? e.pagePath : '/' + e.pagePath;
// 已经在目标页则不重复跳转
if (currentPath === targetPath) return;
uni.reLaunch({ url: targetPath });
},
// #endif
onLoad(option) {
// #ifdef H5
// H5地图选择位置回调数据
if (option.module && option.module == 'locationPicker') {
option.name = ''; // 清空地址
}
// #endif
this.name = option.name || 'DIY_VIEW_INDEX'; // 根据店铺运营模式,打开平台(shop)或者连锁门店(stroe)页面,接口会自行判断
// 获取店铺信息
this.shopInfo = uni.getStorageSync('shopInfo');
// 每次进入页面重置 diy-group 加载状态, 避免缓存的 ready 导致水印提前显示
this.diyGroupReady = false;
},
onShow() {
// 获取关注公众号二维码
this.getFollowQrcode();
// 重新获取店铺信息
this.shopInfo = uni.getStorageSync('shopInfo');
},
mounted() {
// 监听图片、文字、商品列表等组件的点击事件
this.unsubscribe = this.$eventBus?.onMany([
'rubik-cube-tap',
'picture-tap',
'text-tap',
'goods-list-tap',
'presale-tap',
'notes-tap',
'merch-list-tap',
'map-tap',
'img-ads-tap',
'many-goods-list-tap',
'search-tap',
], this._handleDiyGroupInteractionEvent);
},
beforeDestroy() {
// 移除图片点击事件监听
if (this.unsubscribe) this.unsubscribe();
},
methods: {
// 关注公众号
getFollowQrcode() {
if (!this.$util.isWeiXin()) return;
this.$api.sendRequest({
url: '/wechat/api/wechat/followqrcode',
success: res => {
if (res.code >= 0 && res.data) {
this.wechatQrcode = res.data.qrcode;
}
}
});
},
officialAccountsOpen() {
this.$refs.officialAccountsPopup.open();
},
officialAccountsClose() {
this.$refs.officialAccountsPopup.close();
},
// 判断是否需要登录授权
_requireHomeAuth() {
// #ifdef MP-WEIXIN
// 检查 index_login_auth 都开启时才需要登录授权
return this.shopInfo?.index_login_auth === 1;
// #endif
// TODO: 其他平台, 后面可以根据平台类型判断
return false;
},
// 统一处理点击及触摸事件
_handleDiyGroupInteractionEvent(payload) {
// 如果不需要登录授权,直接返回 true 继续传播
if (!this._requireHomeAuth()) {
return true;
}
// 如果已登录,直接返回 true 继续传播
if (this.storeToken) {
return true;
}
this.$refs.login?.open();
// 阻止事件继续传播
return false;
},
// diy-group 渲染完毕回调, 用于触发版本水印等延后显示元素
// 同时通知 splash-state 页面就绪, 启动屏可以关闭
// 注意: diy-group 内部 _notifyLoaded 已有 300ms 渲染 settle,
// 这里不再叠加 500ms, 避免吃掉 splash 的 2s page-ready 兜底预算
// (叠加延迟会导致弱网/冷启动下 splash 走 page-ready-timeout 兜底路径)
onDiyGroupLoaded() {
this.diyGroupReady = true;
try { splashState.notifyPageReady(); } catch (e) {}
},
/**
* 版本连击 3 次以上显示构建版本信息(供测试人员使用)
*/
onVersionTap() {
this._titleTapCount = (this._titleTapCount || 0) + 1;
if (this._titleTapTimer) {
clearTimeout(this._titleTapTimer);
}
if (this._titleTapCount > 3) {
this._titleTapCount = 0;
this._showVersionInfo();
return;
}
// 2 秒内未达到 3 次则重置计数
this._titleTapTimer = setTimeout(() => {
this._titleTapCount = 0;
}, 2000);
},
_showVersionInfo() {
let v = null;
try {
v = typeof __app_version !== 'undefined' ? __app_version : null;
} catch (e) {}
if (v && v.commitId) {
uni.showModal({
title: this.$lang('versionInfo'),
content: `${this.$lang('commitId')}: ${v.commitId}\n${this.$lang('buildTime')}: ${v.buildTime}\n${Config.baseUrl}`,
showCancel: false
});
} else {
console.error('未获取到构建版本信息');
}
}
}
}