修复背景:
账号注销成功后, 用户被重定向到 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 个测试文件全部通过
137 lines
5.2 KiB
JavaScript
137 lines
5.2 KiB
JavaScript
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
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
};
|