fix: some
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 2m59s

This commit is contained in:
luo
2026-10-13 14:53:19 +08:00
parent 77b0e25011
commit da95a914e3
31 changed files with 1702 additions and 116 deletions

View File

@@ -5,12 +5,14 @@
<view class="navbar-back" @tap="goBack">
<image class="back-icon" src="../../static/left.png" mode="aspectFit" />
</view>
<view class="navbar-title">绑定手机号</view>
<view class="navbar-title">{{ bindingTitle }}</view>
<view class="navbar-placeholder"></view>
</view>
<view class="container" :style="{ paddingTop: fromLogin ? '0' : '0' }">
<view class="card flex-col-g20">
<view class="binding-description">{{ bindingDescription }}</view>
<view v-if="assetConflict" class="binding-conflict">该资产已绑定手机号,如需更换请联系管理员</view>
<view class="flex-row-g20">
<label for="">手机号:</label>
<up-input placeholder="请输入绑定的手机号" border="surround" v-model="bind.phone" />
@@ -20,13 +22,13 @@
<label for="">验证码:</label>
<up-input placeholder="验证码" v-model="bind.code">
<template #suffix>
<up-button @tap="getCode" :text="codeText" type="success" :disabled="cooldown > 0"></up-button>
<up-button @tap="getCode" :text="codeText" type="success" :disabled="cooldown > 0 || assetConflict || initializing || bindingMode === 'none'"></up-button>
</template>
</up-input>
</view>
<view class="btn">
<up-button type="primary" @tap="bindPhone">绑定手机号</up-button>
<up-button type="primary" @tap="bindPhone" :loading="submitting" :disabled="initializing || submitting || assetConflict || bindingMode === 'none'">{{ bindingTitle }}</up-button>
</view>
<!-- 如果从登录页面跳转过来,显示退出按钮 -->
@@ -39,14 +41,31 @@
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue';
import { authApi, assetApi } from '@/api/index.js';
import { useUserStore } from '@/store/index.js';
import { setPhoneBindStatus } from '@/utils/phone-bind.js';
import { getPhoneBindingMode } from '@/utils/phone-bind.js';
import { getSessionContext, isCurrentSession } from '@/utils/session-context.js';
const POST_BIND_RELOGIN_NOTICE_KEY = 'postBindReloginNotice';
const userStore = useUserStore();
const initializing = ref(true);
const submitting = ref(false);
const assetConflict = ref(false);
const bindingMode = computed(() => getPhoneBindingMode(userStore.state.customerPhone, userStore.state.assetPhoneBound));
const bindingTitle = computed(() => {
if (bindingMode.value === 'customer') return '绑定客户手机号';
if (bindingMode.value === 'asset') return '给资产绑定手机号';
return '绑定手机号';
});
const bindingDescription = computed(() => ({
customer: '请绑定您自己的手机号,当前资产的手机号关联不会改变。',
asset: '请给当前资产绑定手机号,可使用与您的客户手机号不同的号码。',
'customer-asset': '验证一次手机号,将同时绑定您的客户账号和当前资产。',
none: '您的客户账号和当前资产均已绑定手机号,无需重复绑定。',
unknown: '请验证手机号。客户与资产的绑定将由系统按当前状态处理,已有的资产手机号不会被替换。'
}[bindingMode.value]));
let bind = reactive({
phone: '',
@@ -65,21 +84,33 @@
const options = currentPage.options;
fromLogin.value = options.fromLogin === 'true';
// 契约变化:账号已有主手机号时,主号 + 验证码有效 → 幂等建联。
// 预填账号已有主手机号,避免存量用户「提示要验证却提交任何号码都被拒」的死锁。
if (userStore.state.customerPhone) bind.phone = userStore.state.customerPhone;
// Mandatory binding ends with a new login. Do not consume the day's
// one-time expiry reminder here merely to prefill a phone input.
if (fromLogin.value) {
initializing.value = false;
return;
}
const token = uni.getStorageSync('token');
const identifier = userStore.state.identifier || uni.getStorageSync('identifier') || '';
if (!token || !identifier) return;
if (!token || !identifier) {
initializing.value = false;
return;
}
const context = getSessionContext();
try {
const data = await assetApi.getInfo(identifier);
const mainPhone = (data && data.bound_phone) || '';
if (!isCurrentSession(context)) return;
const mainPhone = data?.bound_phone || '';
if (mainPhone) {
bind.phone = mainPhone;
}
} catch (e) {
// 预填失败不阻塞绑定流程,用户仍可手动输入手机号
console.error('预填主手机号失败', e);
console.error('预填客户手机号失败');
} finally {
initializing.value = false;
}
});
@@ -101,6 +132,7 @@
};
const getCode = async () => {
if (cooldown.value > 0 || initializing.value || assetConflict.value || bindingMode.value === 'none') return;
if (!bind.phone) {
uni.showToast({ title: '请输入手机号', icon: 'none' });
return;
@@ -111,8 +143,10 @@
return;
}
const context = getSessionContext();
try {
const data = await authApi.sendCode(bind.phone, 'bind_phone');
if (!isCurrentSession(context)) return;
cooldown.value = data.cooldown_seconds || 60;
codeText.value = `${cooldown.value}s`;
@@ -133,15 +167,24 @@
};
const bindPhone = async () => {
if (initializing.value || submitting.value || assetConflict.value || bindingMode.value === 'none') return;
if (!bind.phone || !bind.code) {
uni.showToast({ title: '手机号和验证码都不能为空', icon: 'none' });
return;
}
if (!/^1[3-9]\d{9}$/.test(bind.phone)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
return;
}
const context = getSessionContext();
const mode = bindingMode.value;
const submittedPhone = bind.phone;
submitting.value = true;
try {
await authApi.bindPhone(bind.phone, bind.code);
const identifier = userStore.state.identifier || uni.getStorageSync('identifier') || '';
setPhoneBindStatus(identifier, bind.phone);
await authApi.bindPhone(submittedPhone, bind.code);
if (!isCurrentSession(context)) return;
userStore.completePhoneBinding(submittedPhone, mode);
if (fromLogin.value) {
prepareManualRelogin();
@@ -152,10 +195,18 @@
uni.showToast({ title: '绑定成功', icon: 'success' });
setTimeout(() => {
// 从其他页面过来的,返回上一页
uni.navigateBack();
if (isCurrentSession(context)) uni.navigateBack();
}, 1500);
} catch (e) {
console.error('绑定手机号失败', e);
if (!isCurrentSession(context)) return;
if (Number(e?.code) === 1192) {
assetConflict.value = true;
uni.showModal({ title: '无法更换资产手机号', content: '该资产已绑定手机号,如需更换请联系管理员', showCancel: false });
} else {
uni.showToast({ title: e?.msg || e?.message || '绑定失败,请稍后重试', icon: 'none' });
}
} finally {
submitting.value = false;
}
};
@@ -165,12 +216,14 @@
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
userStore.clearUser();
uni.clearStorageSync();
uni.reLaunch({ url: '/pages/login/login' });
}
}
});
};
onUnmounted(() => clearInterval(timer));
const goBack = () => {
uni.navigateTo({
@@ -237,4 +290,6 @@
.container {
padding: var(--space-md);
}
.binding-description { color: var(--text-secondary); font-size: 26rpx; line-height: 1.6; }
.binding-conflict { color: var(--danger); font-size: 26rpx; line-height: 1.6; }
</style>