Files
lucky_shop/pages_tool/member/cancelstatus.vue
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

192 lines
4.7 KiB
Vue

<template>
<view class="container" :style="themeColor">
<view class="cancelstatus-wrap">
<view class="cancelstatus-block">
<view class="cancelstatus-box">
<view class="cancelstatus-box-sort color-base-bg">1</view>
<view class="cancelstatus-box-con">
<view class="cancelstatus-box-name">{{ $lang('member.cancelstatus.step1Name') }}</view>
<view class="cancelstatus-box-info">{{ $lang('member.cancelstatus.step1Desc') }}</view>
</view>
<view class="cancelstatus-box-line color-base-bg"></view>
</view>
<view class="cancelstatus-box">
<view class="cancelstatus-box-sort color-base-bg">2</view>
<view class="cancelstatus-box-con">
<view class="cancelstatus-box-name">{{ $lang('member.cancelstatus.step2Name') }}</view>
<view class="cancelstatus-box-info">{{ $lang('member.cancelstatus.step2Desc') }}</view>
</view>
<view class="cancelstatus-box-line color-base-bg" :class="{ 'opacity-4': state == 0 }"></view>
</view>
<view class="cancelstatus-box cancelstatus-box-last">
<view class="cancelstatus-box-sort color-base-bg" :class="[ state == 1 ? 'opacity': 'opacity-4' ]">
3
</view>
<view class="cancelstatus-box-con">
<view class="cancelstatus-box-name">{{ $lang('member.cancelstatus.step3Name') }}</view>
<view class="cancelstatus-box-info">{{ $lang('member.cancelstatus.step3Desc') }}</view>
</view>
</view>
</view>
<view class="cancelstatus-btn" v-if="state == 0">
<button type="primary" @click="back">{{ $lang('member.cancelstatus.back') }}</button>
<button class="color-base-bg" @click="revoke">{{ $lang('member.cancelstatus.revokeApplication') }}</button>
</view>
</view>
</view>
</template>
<script>
import memberPublic from './public/js/member-public.js';
export default {
mixins: [memberPublic],
data() {
return {
state: 0
};
},
onLoad(option) {
// 判断登录
if (!this.storeToken) {
// 跳登录入口 (mixin 提供, 内部带 800ms 防抖锁 + 跨平台分流:
// 微信小程序 / 鸿蒙下跳首页 INDEX_PAGE_URL, 走 ns-login 原生授权弹窗;
// H5 / APP 下跳原生登录页 LOGIN_PAGE_URL)
this.redirectToLoginOnce();
} else {
this.getStatus();
}
},
onShow() {
this.getStatus();
},
methods: {
getStatus() {
this.$api.sendRequest({
url: '/membercancel/api/membercancel/info',
success: res => {
if (res.code >= 0 && res.data) {
this.state = res.data.status;
if (this.state == -1) {
this.$util.redirectTo('/pages_tool/member/cancelrefuse');
}
}
if (res.code == -1) {
// 跳登录入口 (mixin 提供, 内部带 800ms 防抖锁 + 跨平台分流)
this.redirectToLoginOnce();
}
}
});
},
back() {
this.$util.redirectTo(this.$util.MEMBER_PAGE_URL);
},
revoke() {
uni.showModal({
title: this.$lang('member.cancelstatus.riskTip'),
content: this.$lang('member.cancelstatus.revokeConfirm'),
confirmColor: '#000000',
success: res => {
if (res.confirm) {
this.$api.sendRequest({
url: '/membercancel/api/membercancel/cancelApply',
success: res => {
if (res.code >= 0) {
this.$util.redirectTo(this.$util.MEMBER_PAGE_URL);
}
}
});
}
}
});
}
}
};
</script>
<style lang="scss" scoped>
.cancelstatus-wrap {
.cancelstatus-block {
padding: 50rpx;
}
.cancelstatus-box {
position: relative;
display: flex;
height: 200rpx;
.cancelstatus-box-sort {
width: 36rpx;
height: 36rpx;
text-align: center;
line-height: 36rpx;
border-radius: 50%;
color: #ffffff;
margin-right: 17rpx;
font-size: 24rpx;
}
.opacity {
opacity: 1;
&-4 {
opacity: 0.4;
}
}
.cancelstatus-box-name {
font-size: 32rpx;
line-height: 32rpx;
margin-top: 3rpx;
}
.cancelstatus-box-info {
margin-top: 15rpx;
color: #666666;
font-size: 28rpx;
}
.cancelstatus-box-line {
position: absolute;
width: 2rpx;
height: 164rpx;
top: 36rpx;
left: 18rpx;
}
&.cancelstatus-box-last {
height: 80rpx;
}
}
.cancelstatus-btn {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
bottom: 0;
width: 100%;
height: 150rpx;
button {
width: 300rpx;
height: 80rpx;
font-size: 28rpx;
line-height: 80rpx;
margin: 0 15rpx;
border-radius: $border-radius;
}
button[type='primary'] {
background-color: unset !important;
color: #333333;
border: 2rpx solid #dddddd;
}
button:nth-child(2) {
color: #ffffff;
}
}
}
</style>