Files
device-voice-h5/pages/bind/bind.vue
luo da95a914e3
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 2m59s
fix: some
2026-10-13 14:53:19 +08:00

296 lines
8.7 KiB
Vue
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.
<template>
<view class="page">
<!-- 自定义导航栏 -->
<view v-if="!fromLogin" class="custom-navbar">
<view class="navbar-back" @tap="goBack">
<image class="back-icon" src="../../static/left.png" mode="aspectFit" />
</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" />
</view>
<view class="flex-row-g20">
<label for="">验证码:</label>
<up-input placeholder="验证码" v-model="bind.code">
<template #suffix>
<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" :loading="submitting" :disabled="initializing || submitting || assetConflict || bindingMode === 'none'">{{ bindingTitle }}</up-button>
</view>
<!-- 如果从登录页面跳转过来,显示退出按钮 -->
<view class="btn" v-if="fromLogin">
<up-button type="error" @tap="logout">退出</up-button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue';
import { authApi, assetApi } from '@/api/index.js';
import { useUserStore } from '@/store/index.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: '',
code: ''
});
let cooldown = ref(0);
let codeText = ref('获取验证码');
let timer = null;
let fromLogin = ref(false);
onMounted(async () => {
// 获取页面参数,判断是否从登录页面跳转过来
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
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) {
initializing.value = false;
return;
}
const context = getSessionContext();
try {
const data = await assetApi.getInfo(identifier);
if (!isCurrentSession(context)) return;
const mainPhone = data?.bound_phone || '';
if (mainPhone) {
bind.phone = mainPhone;
}
} catch (e) {
// 预填失败不阻塞绑定流程,用户仍可手动输入手机号
console.error('预填客户手机号失败');
} finally {
initializing.value = false;
}
});
const prepareManualRelogin = () => {
const preservedIdentifier = userStore.state.identifier || uni.getStorageSync('identifier') || '';
userStore.clearUser();
if (preservedIdentifier) {
userStore.setIdentifier(preservedIdentifier);
}
uni.setStorageSync(POST_BIND_RELOGIN_NOTICE_KEY, '1');
// #ifdef H5
if (typeof sessionStorage !== 'undefined') {
sessionStorage.removeItem('assetToken');
}
// #endif
};
const getCode = async () => {
if (cooldown.value > 0 || initializing.value || assetConflict.value || bindingMode.value === 'none') return;
if (!bind.phone) {
uni.showToast({ title: '请输入手机号', icon: 'none' });
return;
}
if (!/^1[3-9]\d{9}$/.test(bind.phone)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
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`;
timer = setInterval(() => {
cooldown.value--;
if (cooldown.value <= 0) {
clearInterval(timer);
codeText.value = '获取验证码';
} else {
codeText.value = `${cooldown.value}s`;
}
}, 1000);
uni.showToast({ title: '验证码已发送', icon: 'success' });
} catch (e) {
console.error('发送验证码失败', e);
}
};
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(submittedPhone, bind.code);
if (!isCurrentSession(context)) return;
userStore.completePhoneBinding(submittedPhone, mode);
if (fromLogin.value) {
prepareManualRelogin();
uni.reLaunch({ url: '/pages/login/login' });
return;
}
uni.showToast({ title: '绑定成功', icon: 'success' });
setTimeout(() => {
// 从其他页面过来的,返回上一页
if (isCurrentSession(context)) uni.navigateBack();
}, 1500);
} catch (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;
}
};
const logout = () => {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
userStore.clearUser();
uni.clearStorageSync();
uni.reLaunch({ url: '/pages/login/login' });
}
}
});
};
onUnmounted(() => clearInterval(timer));
const goBack = () => {
uni.navigateTo({
url: '/pages/index/index'
})
};
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: var(--bg-secondary);
max-width: 750rpx;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
}
.custom-navbar {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
margin-bottom: 24rpx;
background: var(--bg-primary);
border-bottom: 1rpx solid var(--border-light);
padding: 0 24rpx;
position: sticky;
top: 0;
z-index: 100;
.navbar-back {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx;
cursor: pointer;
.back-icon {
width: 40rpx;
height: 40rpx;
}
.back-text {
font-size: 28rpx;
color: var(--text-primary);
}
}
.navbar-title {
font-size: 32rpx;
font-weight: 600;
color: var(--text-primary);
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.navbar-placeholder {
width: 100rpx;
}
}
.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>