Files
ks-app-employment-service/pages/login/sms-verify.vue
2026-04-10 11:25:34 +08:00

579 lines
17 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="sms-verify-page">
<!-- 顶部导航栏 -->
<!-- <view class="nav-bar">
<view class="nav-back" @click="goBack">
<uni-icons type="arrowleft" size="24" color="#333"></uni-icons>
</view>
<view class="nav-title">短信验证</view>
<view class="nav-placeholder"></view>
</view> -->
<!-- 页面内容 -->
<view class="page-content">
<!-- 安全提示 -->
<view class="security-tip">
<view class="tip-icon">
<uni-icons type="auth-filled" size="36" color="#256BFA"></uni-icons>
</view>
<view class="tip-text">为了您的账户安全需要您进行验证</view>
</view>
<!-- 手机号显示 -->
<view class="phone-display">
<view class="phone-label">验证码将发送至</view>
<view class="phone-number">{{ formattedPhone }}</view>
</view>
<!-- 验证码输入区域 -->
<view class="sms-input-area">
<view class="input-label">请输入6位数字验证码</view>
<view class="single-input-container">
<input
ref="smsInput"
class="single-input"
type="tel"
inputmode="numeric"
pattern="[0-9]*"
autocomplete="one-time-code"
maxlength="6"
:value="smsCode"
@input="onCodeInput"
@paste="onPaste"
placeholder="请输入验证码"
:focus="autoFocus"
/>
</view>
</view>
<!-- 倒计时和重发 -->
<view class="countdown-section">
<view v-if="countdown > 0" class="countdown-text">
{{ countdown }}秒后重新获取
</view>
<view v-else class="resend-text" @click="resendSms">
重新获取验证码
</view>
</view>
<!-- 提交按钮 -->
<button class="submit-btn" :disabled="!canSubmit" @click="submitVerification">
<text v-if="!loading">确认</text>
<view v-else class="loading-spinner"></view>
</button>
</view>
</view>
</template>
<script setup>
import { ref, inject, computed, onMounted, onUnmounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
const { $api } = inject('globalFunction');
// 从页面参数获取数据
const phone = ref('');
const openid = ref('');
const unionid = ref('');
const userType = ref('');
const orgType = ref('');
// 验证码相关
const smsCode = ref(''); // 单个字符串验证码
const smsInput = ref(null); // 输入框引用
const countdown = ref(60); // 倒计时
const timer = ref(null);
const loading = ref(false);
const autoFocus = ref(true); // 自动聚焦
// 格式化手机号:只显示前三位和后四位,中间用星号代替
const formattedPhone = computed(() => {
if (!phone.value || phone.value.trim() === '') {
return '未知号码';
}
// 清理手机号,移除空格和特殊字符
const cleanPhone = phone.value.replace(/\D/g, '');
if (cleanPhone.length < 11) {
return phone.value;
}
const prefix = cleanPhone.substring(0, 3);
const suffix = cleanPhone.substring(cleanPhone.length - 4);
return `${prefix}****${suffix}`;
});
// 是否可以提交
const canSubmit = computed(() => {
return smsCode.value.length === 6 && !loading.value;
});
// 页面加载时获取参数
onLoad(async (options) => {
// 尝试多种可能的手机号字段名(按优先级)
const possiblePhoneFields = [
'phone', 'mobile', 'phoneNumber', 'tel',
'phoneNum', 'userPhone', 'telephone', 'mobilePhone', 'cellphone',
'userTel', 'userMobile', 'contactPhone'
];
let foundPhone = '';
for (const field of possiblePhoneFields) {
if (options[field]) {
foundPhone = options[field];
break;
}
}
// 如果没找到尝试从URL参数中解析
if (!foundPhone) {
const currentPages = getCurrentPages();
if (currentPages.length > 0) {
const currentPage = currentPages[currentPages.length - 1];
// 尝试从页面路由参数中获取
if (currentPage.$page && currentPage.$page.fullPath) {
const urlParams = new URLSearchParams(currentPage.$page.fullPath.split('?')[1] || '');
for (const field of possiblePhoneFields) {
const value = urlParams.get(field);
if (value) {
foundPhone = value;
break;
}
}
}
}
}
phone.value = foundPhone || '';
openid.value = options.openid || '';
unionid.value = options.unionid || '';
userType.value = options.userType || '';
orgType.value = options.orgType || '';
// 如果手机号仍然为空,显示错误信息
if (!phone.value) {
uni.showToast({ title: '无法获取手机号,请返回重试', icon: 'none' });
}
// 开始倒计时
startCountdown();
// 自动聚焦到输入框
setTimeout(() => {
if (smsInput.value && typeof smsInput.value.focus === 'function') {
smsInput.value.focus();
}
}, 300);
});
// 开始倒计时
const startCountdown = () => {
clearInterval(timer.value);
countdown.value = 60;
timer.value = setInterval(() => {
if (countdown.value > 0) {
countdown.value--;
} else {
clearInterval(timer.value);
}
}, 1000);
};
// 重发验证码
const resendSms = async () => {
if (countdown.value > 0) return;
// 检查手机号是否为空
if (!phone.value) {
uni.showToast({ title: '手机号获取失败,请返回重试', icon: 'none' });
return;
}
uni.showLoading({ title: '发送中...' });
try {
// 调用重新发送验证码接口
await $api.createRequest('/app/sendSmsAgain', { phone: phone.value }, 'post');
uni.hideLoading();
uni.showToast({ title: '验证码已发送', icon: 'success' });
startCountdown();
} catch (error) {
uni.hideLoading();
uni.showToast({ title: error.msg || '发送失败', icon: 'none' });
}
};
// 验证码输入处理
const onCodeInput = (e) => {
let value = e.detail.value.replace(/\D/g, ''); // 只保留数字
// 限制为6位数字
if (value.length > 6) {
value = value.substring(0, 6);
}
smsCode.value = value;
};
// 粘贴事件处理
const onPaste = (e) => {
// #ifdef MP-WEIXIN
// 微信小程序中,使用异步方式获取剪贴板
setTimeout(() => {
pasteFromClipboard();
}, 50);
// 尝试阻止默认行为
if (e && e.preventDefault) {
e.preventDefault();
}
if (e && e.stopPropagation) {
e.stopPropagation();
}
// #endif
// #ifndef MP-WEIXIN
// 其他平台H5/App使用标准粘贴处理
e.preventDefault();
e.stopPropagation();
let pasteText = '';
if (e.clipboardData && e.clipboardData.getData) {
pasteText = e.clipboardData.getData('text');
}
if (pasteText) {
// 处理粘贴的文本
handlePaste(pasteText);
} else {
// 使用异步方式获取剪贴板
setTimeout(() => {
pasteFromClipboard();
}, 100);
}
return false;
// #endif
};
// 处理粘贴的文本
const handlePaste = (text) => {
// 提取数字
const digits = text.replace(/\D/g, '');
if (digits.length === 0) {
uni.showToast({ title: '剪贴板中没有找到验证码', icon: 'none' });
return;
}
// 限制为6位数字
const code = digits.substring(0, 6);
smsCode.value = code;
uni.showToast({ title: '已粘贴验证码', icon: 'success' });
};
// 从剪贴板粘贴验证码
const pasteFromClipboard = () => {
uni.getClipboardData({
success: (res) => {
const text = res.data || '';
handlePaste(text);
},
fail: () => {
uni.showToast({ title: '读取剪贴板失败', icon: 'none' });
}
});
};
// 提交验证
const submitVerification = async () => {
if (!canSubmit.value) {
return;
}
loading.value = true;
const code = smsCode.value;
// 检查手机号是否为空
if (!phone.value) {
uni.showToast({ title: '手机号获取失败,请返回重试', icon: 'none' });
loading.value = false;
return;
}
try {
// 调用接口 /app/appLoginPhone
const res = await $api.createRequest('/app/appLoginPhone', {
smsCode: code,
phone: phone.value,
openid: openid.value,
unionid: unionid.value,
userType: userType.value,
orgType: orgType.value
}, 'post');
if (res.token) {
// 登录成功存储token
const userStore = useUserStore();
await userStore.loginSetToken(res.token);
// 更新用户类型到缓存
if (res.isCompanyUser !== undefined) {
const userInfo = uni.getStorageSync('userInfo') || {};
userInfo.isCompanyUser = res.isCompanyUser ? 0 : 1;
uni.setStorageSync('userInfo', userInfo);
}
uni.showToast({ title: '登录成功', icon: 'success' });
// 返回上一页或跳转到首页
setTimeout(() => {
uni.navigateBack({ delta: 2 }); // 返回两页(跳过登录页面)
}, 1500);
} else {
uni.showToast({ title: res.msg || '验证失败', icon: 'none' });
}
} catch (error) {
uni.showToast({ title: error.msg || '验证失败,请重试', icon: 'none' });
} finally {
loading.value = false;
}
};
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
// 组件销毁时清除定时器
onUnmounted(() => {
clearInterval(timer.value);
});
// 需要导入useUserStore
import useUserStore from '@/stores/useUserStore';
</script>
<style lang="stylus" scoped>
.sms-verify-page
min-height: 100vh
background: linear-gradient(135deg, #F7F9FF 0%, #FFFFFF 100%)
.nav-bar
display: flex
align-items: center
justify-content: space-between
height: 88rpx
padding: 0 32rpx
background: #FFFFFF
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04)
.nav-back
width: 60rpx
height: 60rpx
display: flex
align-items: center
justify-content: center
border-radius: 50%
transition: background-color 0.2s ease
&:active
background-color: #F5F5F5
.nav-title
font-size: 32rpx
font-weight: 600
color: #333333
.nav-placeholder
width: 60rpx
.page-content
padding: 80rpx 48rpx 48rpx
.security-tip
display: flex
flex-direction: column
align-items: center
justify-content: center
margin-bottom: 60rpx
padding: 32rpx
background: linear-gradient(135deg, rgba(37, 107, 250, 0.08) 0%, rgba(30, 91, 255, 0.04) 100%)
border-radius: 24rpx
border: 1rpx solid rgba(37, 107, 250, 0.1)
.tip-icon
margin-bottom: 20rpx
width: 64rpx
height: 64rpx
display: flex
align-items: center
justify-content: center
background: #FFFFFF
border-radius: 50%
box-shadow: 0 4rpx 16rpx rgba(37, 107, 250, 0.15)
.tip-text
font-size: 28rpx
font-weight: 500
color: #256BFA
text-align: center
line-height: 1.4
.phone-display
text-align: center
margin-bottom: 80rpx
.phone-label
font-size: 28rpx
color: #666666
margin-bottom: 16rpx
opacity: 0.8
.phone-number
font-size: 44rpx
font-weight: 700
color: #333333
letter-spacing: 2rpx
background: linear-gradient(135deg, #256BFA 0%, #1E5BFF 100%)
-webkit-background-clip: text
-webkit-text-fill-color: transparent
background-clip: text
.sms-input-area
margin-bottom: 60rpx
.input-label
font-size: 28rpx
color: #666666
text-align: center
margin-bottom: 24rpx
font-weight: 500
.single-input-container
margin-bottom: 16rpx
.single-input
width: 100%
height: 120rpx
border: 3rpx solid #E8ECF4
border-radius: 20rpx
font-size: 48rpx
font-weight: 700
color: #333333
background: #FFFFFF
text-align: center
padding: 0 32rpx
box-sizing: border-box
outline: none
caret-color: #256BFA
transition: all 0.3s ease
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05)
&:focus
border-color: #256BFA
background: #FFFFFF
box-shadow: 0 8rpx 32rpx rgba(37, 107, 250, 0.2)
transform: translateY(-2rpx)
&::placeholder
color: #CCCCCC
font-size: 32rpx
font-weight: normal
.countdown-section
text-align: center
margin-bottom: 80rpx
.countdown-text
font-size: 28rpx
color: #999999
font-weight: 500
.resend-text
font-size: 28rpx
color: #256BFA
font-weight: 600
cursor: pointer
padding: 12rpx 24rpx
border-radius: 24rpx
background: rgba(37, 107, 250, 0.1)
display: inline-block
transition: all 0.2s ease
&:active
background: rgba(37, 107, 250, 0.2)
transform: scale(0.98)
.submit-btn
width: 100%
height: 96rpx
background: linear-gradient(135deg, #256BFA 0%, #1E5BFF 100%)
border-radius: 48rpx
color: #FFFFFF
font-size: 34rpx
font-weight: 600
border: none
display: flex
align-items: center
justify-content: center
box-shadow: 0 8rpx 32rpx rgba(37, 107, 250, 0.3)
transition: all 0.3s ease
position: relative
overflow: hidden
&::before
content: ''
position: absolute
top: 0
left: -100%
width: 100%
height: 100%
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent)
transition: left 0.5s ease
&:not(:disabled):hover::before
left: 100%
&:not(:disabled):active
transform: translateY(2rpx)
box-shadow: 0 4rpx 16rpx rgba(37, 107, 250, 0.3)
&:disabled
opacity: 0.5
cursor: not-allowed
box-shadow: none
.loading-spinner
width: 48rpx
height: 48rpx
border: 4rpx solid rgba(255, 255, 255, 0.3)
border-radius: 50%
border-top: 4rpx solid #FFFFFF
animation: spin 1s linear infinite
// 按钮重置样式
button::after
border: none
// 动画定义
@keyframes spin
0%
transform: rotate(0deg)
100%
transform: rotate(360deg)
// 页面进入动画
.sms-verify-page
animation: fadeIn 0.4s ease-out
@keyframes fadeIn
from
opacity: 0
transform: translateY(20rpx)
to
opacity: 1
transform: translateY(0)
</style>