Files
lucky_shop/pages_tool/member/public/js/member-public.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

137 lines
5.2 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.
export default {
methods: {
// 清除登录态(token / 会员信息 / 购物车 / 本地存储)
clearLoginState() {
this.$store.commit('setToken', '');
this.$store.commit('setMemberInfo', '');
this.$store.dispatch('emptyCart');
uni.removeStorageSync('authInfo');
uni.removeStorageSync('privacyAgreed');
},
// 跨平台登录入口:小程序下用 ns-login 原生授权弹窗 (走 wx.login / getPhoneNumber),
// H5 / 快应用 / APP 下用原生登录页 /pages_tool/login/login (手机号+动态码表单).
//
// 修复背景: 之前所有跳转统一用 LOGIN_PAGE_URL (/pages_tool/login/login),
// 这个原生登录页在微信小程序下是 form 提交 (像 H5), 没有走 wx.login 授权。
// 期望行为:
// - 微信小程序 / 鸿蒙元服务 / 其他小程序: 跳到首页 /pages/index/index,
// 首页挂载了 <ns-login ref="login">, 用户后续操作触发 _requireHomeAuth
// 时会自动弹 ns-login 弹窗, 走原生 wx.login / getPhoneNumber 授权.
// - H5 / 快应用 / APP: 保持跳 LOGIN_PAGE_URL (原生登录页), 表单流程正常.
getLoginEntryUrl() {
// #ifdef MP-WEIXIN || MP-HARMONY || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ || MP-KUAISHOU || MP-JD || MP-360 || MP-XHS
return this.$util.INDEX_PAGE_URL;
// #endif
// #ifndef MP-WEIXIN || MP-HARMONY || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ || MP-KUAISHOU || MP-JD || MP-360 || MP-XHS
return this.$util.LOGIN_PAGE_URL;
// #endif
},
// 统一的"跳登录入口"封装, 包含组件级防抖锁 + 清登录态 + reLaunch
//
// 为什么需要防抖: 微信小程序下, 短时间内连发 reLaunch 会触发
// "appLaunch with non-empty page stack" native 错误 (页面销毁未完成, 新 launch 时栈非空).
// 注销/退出流程中常见并发场景:
// - cancelsuccess.vue onLoad 调 getStatus 异步回调 + 用户点 "完成" 按钮
// - 多个 submitCancelApply 同时被触发
// 防抖策略: 同一组件实例 800ms 内只能跳一次, 之后忽略
// (util.js reLaunch 分支还有 300ms 全局锁作为第二道防线)
redirectToLoginOnce() {
// 组件级防抖: 800ms 内重复调用直接忽略
const now = Date.now();
if (this._lastLoginRedirectAt && now - this._lastLoginRedirectAt < 800) {
console.warn('[member-public] redirectToLoginOnce 防抖: 800ms 内重复调用已忽略');
return;
}
this._lastLoginRedirectAt = now;
this.clearLoginState();
// 小程序下 getLoginEntryUrl() 返回首页 (走 ns-login 原生授权弹窗),
// H5 / APP 下返回原生登录页 (form 提交)
this.$util.redirectTo(this.getLoginEntryUrl(), {}, 'reLaunch');
},
// 退出登录
logout() {
uni.showModal({
title: this.$lang('logoutTitle'),
content: this.$lang('logoutContent'),
success: (res) => {
if (res.confirm) {
// 统一的跳登录入口: 内部带防抖锁, 避免连发 reLaunch
this.redirectToLoginOnce();
}
}
});
},
// 账号注销入口:先查状态,根据状态分流
cancellation() {
this.getCancelStatus();
},
// 查询注销状态并跳转对应页面
// status: undefined/null → 未申请 → 协议页 cancellation.vue
// status: 0 → 审核中 → 状态页 cancelstatus.vue
// status: 1 → 通过 → 成功页 cancelsuccess.vue
// status: -1 → 拒绝 → 拒绝页 cancelrefuse.vue
getCancelStatus() {
this.$api.sendRequest({
url: '/membercancel/api/membercancel/info',
success: res => {
if (res.code >= 0) {
if (res.data) {
if (res.data.status == 0) {
this.$util.redirectTo('/pages_tool/member/cancelstatus');
} else if (res.data.status == 1) {
this.$util.redirectTo('/pages_tool/member/cancelsuccess');
} else {
this.$util.redirectTo('/pages_tool/member/cancelrefuse');
}
} else {
this.$util.redirectTo('/pages_tool/member/cancellation');
}
} else if (res.code == -1) {
// 统一的跳登录入口: 内部带防抖锁, 避免连发 reLaunch
this.redirectToLoginOnce();
}
}
});
},
// 提交注销申请并清理登录态(供 assets.vue 等页面调用)
submitCancelApply(options = {}) {
uni.showModal({
title: options.title || this.$lang('member.public.riskWarningTitle'),
content: options.content || this.$lang('member.public.cancelAccountConfirm'),
confirmColor: '#000000',
success: res => {
if (res.confirm) {
this.$api.sendRequest({
url: '/membercancel/api/membercancel/apply',
success: rres => {
if (rres.code >= 0) {
// 注销申请提交成功后立刻清空登录态,
// 避免用户通过标题栏/导航栏返回到会员中心等页面时仍能看到账号信息
if (options.clearStateAndRedirect) {
// 跳登录入口: 内部带防抖锁, 避免连发 reLaunch
this.redirectToLoginOnce();
} else {
// 跳 cancelstatus (审核中), 也走 reLaunch 清栈
this.clearLoginState();
this.$util.redirectTo('/pages_tool/member/cancelstatus', {}, 'reLaunch');
}
} else {
this.$util.showToast({
title: rres.message
});
}
}
});
}
}
});
}
}
};