Files
qingdao-employment-service/pages/auth/index.vue
2025-12-25 15:15:02 +08:00

713 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>
<AppLayout title="">
<template #headerleft v-if="isMiniProgram">
<view >
<image class="btnback" src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
<view class="auth-container">
<view class="auth-header">
<view class="auth-title">身份认证</view>
<view class="auth-subtitle">请填写您的身份信息进行验证</view>
</view>
<view class="auth-form">
<view class="form-item" >
<view class="form-label">
身份证号
<text v-if="idCardError" class="error-text">{{ idCardError }}</text>
</view>
<view class="form-input-wrapper" :class="{ 'error': idCardError }">
<input
class="form-input"
type="idcard"
v-model="formData.idCard"
placeholder="请输入18位身份证号码"
maxlength="18"
@input="onIdCardInput"
@blur="validateIdCard"
/>
<view class="input-clear" v-if="formData.idCard" @click="clearField('idCard')">
<my-icons type="close-circle-filled" size="36" color="#ccc"></my-icons>
</view>
</view>
</view>
<!-- 手机号 -->
<view class="form-item" >
<view class="form-label">
手机号
<text v-if="phoneError" class="error-text">{{ phoneError }}</text>
</view>
<view class="form-input-wrapper" :class="{ 'error': phoneError }">
<input
class="form-input"
type="number"
v-model="formData.phone"
placeholder="请输入11位手机号码"
maxlength="11"
@input="onPhoneInput"
@blur="validatePhone"
/>
<view class="input-clear" v-if="formData.phone" @click="clearField('phone')">
<my-icons type="close-circle-filled" size="36" color="#ccc"></my-icons>
</view>
</view>
</view>
<!-- 验证码 -->
<view class="form-item" >
<view class="form-label">
验证码
<text v-if="codeError" class="error-text">{{ codeError }}</text>
</view>
<view class="form-input-wrapper" :class="{ 'error': codeError }">
<input
class="form-input code-input"
type="number"
v-model="formData.code"
placeholder="请输入验证码"
maxlength="6"
@input="onCodeInput"
@blur="validateCode"
/>
<view class="send-code-btn"
:class="{ 'disabled': !canSendCode }"
@click="sendCode">
{{ codeBtnText }}
</view>
</view>
</view>
</view>
<!-- 认证按钮 -->
<view class="auth-btn-container">
<button class="auth-btn" :class="{ 'disabled': !canSubmit }" @click="submitAuth">
确认认证
</button>
<view class="auth-tips">
认证信息仅用于身份验证我们将严格保护您的隐私
</view>
</view>
<!-- 认证状态提示 -->
<view v-if="authStatus" class="auth-status" :class="authStatus.type">
<my-icons :type="authStatus.icon" size="48" :color="authStatus.color"></my-icons>
<text class="status-text">{{ authStatus.message }}</text>
</view>
</view>
</AppLayout>
</template>
<script setup>
import { reactive, ref, computed, onMounted ,onUnmounted ,inject} from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
import { playTextDirectly } from '@/hook/useTTSPlayer-all-in-one';
const { $api ,navBack} = inject('globalFunction');
const { isMiniProgram } = storeToRefs(useUserStore());
// 表单数据
const formData = ref({
idCard: '',
phone: '',
code: ''
});
// 错误提示
const idCardError = ref('');
const phoneError = ref('');
const codeError = ref('');
// 验证码倒计时
const codeCountdown = ref(0);
const codeTimer = ref(null);
const codeBtnText = ref('发送验证码');
// 认证状态
const authStatus = ref(null);
const canSendCode = computed(() => {
return codeCountdown.value === 0 && formData.value.phone.length === 11 && !phoneError.value;
});
const canSubmit = computed(() => {
return formData.value.idCard && formData.value.phone && formData.value.code &&
!idCardError.value && !phoneError.value && !codeError.value;
});
// 身份证输入处理
const onIdCardInput = (e) => {
formData.value.idCard = e.detail.value.toUpperCase();
idCardError.value = '';
};
// 手机号输入处理
const onPhoneInput = (e) => {
formData.value.phone = e.detail.value.replace(/[^\d]/g, '');
phoneError.value = '';
// 如果手机号变化且验证码已发送,清空验证码
if (formData.value.code) {
formData.value.code = '';
codeError.value = '';
}
};
// 验证码输入处理
const onCodeInput = (e) => {
formData.value.code = e.detail.value.replace(/[^\d]/g, '');
codeError.value = '';
};
// 清空字段
const clearField = (field) => {
formData.value[field] = '';
switch(field) {
case 'idCard':
idCardError.value = '';
break;
case 'phone':
phoneError.value = '';
break;
case 'code':
codeError.value = '';
break;
}
};
// 验证身份证号
const validateIdCard = () => {
if (!formData.value.idCard) {
idCardError.value = '请输入身份证号码';
return false;
}
const idCard = formData.value.idCard.trim();
if (idCard.length !== 18) {
idCardError.value = '身份证号码必须为18位';
return false;
}
// 身份证号格式校验
const idCardPattern = /^\d{17}[\dXx]$/;
if (!idCardPattern.test(idCard)) {
idCardError.value = '身份证号码格式不正确';
return false;
}
// 校验码验证
if (!validateIdCardCheckCode(idCard)) {
idCardError.value = '身份证号码校验失败';
return false;
}
idCardError.value = '';
return true;
};
// 身份证校验码验证
const validateIdCardCheckCode = (idCard) => {
if (idCard.length !== 18) return false;
const weight = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
let sum = 0;
for (let i = 0; i < 17; i++) {
sum += parseInt(idCard[i]) * weight[i];
}
const checkCode = checkCodes[sum % 11];
return checkCode === idCard[17].toUpperCase();
};
// 验证手机号
const validatePhone = () => {
if (!formData.value.phone) {
phoneError.value = '请输入手机号码';
return false;
}
const phone = formData.value.phone.trim();
if (phone.length !== 11) {
phoneError.value = '手机号码必须为11位';
return false;
}
// 手机号格式校验
const phonePattern = /^1[3-9]\d{9}$/;
if (!phonePattern.test(phone)) {
phoneError.value = '手机号码格式不正确';
return false;
}
phoneError.value = '';
return true;
};
// 验证验证码
const validateCode = () => {
if (!formData.value.code) {
codeError.value = '请输入验证码';
return false;
}
if (formData.value.code.length < 4) {
codeError.value = '请输入正确的验证码';
return false;
}
codeError.value = '';
return true;
};
// 发送验证码
const sendCode = async () => {
if (!canSendCode.value) {
if (!formData.value.phone) {
phoneError.value = '请输入手机号码';
} else if (formData.value.phone.length !== 11) {
phoneError.value = '手机号码必须为11位';
} else if (phoneError.value) {
// 已有错误提示
}
return;
}
// 验证手机号格式
if (!validatePhone()) {
return;
}
// 开始倒计时
codeCountdown.value = 60;
updateCodeBtnText();
codeTimer.value = setInterval(() => {
codeCountdown.value--;
updateCodeBtnText();
if (codeCountdown.value <= 0) {
clearInterval(codeTimer.value);
codeTimer.value = null;
}
}, 1000);
try {
// 调用发送短信验证码接口
const params = {
phone: formData.value.phone,
type: 'auth' // 身份认证类型
};
await $api.createRequest('/app/auth/send-code', params, 'post');
// 发送成功提示
showAuthStatus('success', '验证码已发送至您的手机');
playTextDirectly('验证码已发送,请注意查收');
} catch (error) {
// 发送失败,重置倒计时
codeCountdown.value = 0;
clearInterval(codeTimer.value);
codeTimer.value = null;
updateCodeBtnText();
// 错误提示
showAuthStatus('error', '验证码发送失败,请重试');
playTextDirectly('验证码发送失败');
}
};
// 更新验证码按钮文字
const updateCodeBtnText = () => {
if (codeCountdown.value > 0) {
codeBtnText.value = `${codeCountdown.value}s后重新发送`;
} else {
codeBtnText.value = '发送验证码';
}
};
// 显示认证状态
const showAuthStatus = (type, message) => {
const statusMap = {
success: {
icon: 'check-circle-filled',
color: '#52c41a',
message
},
error: {
icon: 'close-circle-filled',
color: '#ff4d4f',
message
},
loading: {
icon: 'loading',
color: '#1677ff',
message
}
};
authStatus.value = {
type,
...statusMap[type]
};
// 3秒后自动清除成功/错误状态
if (type !== 'loading') {
setTimeout(() => {
authStatus.value = null;
}, 3000);
}
};
// 提交认证
const submitAuth = async () => {
if (!canSubmit.value) {
// 触发表单验证
validateIdCard();
validatePhone();
validateCode();
return;
}
// 显示加载状态
showAuthStatus('loading', '正在认证中...');
try {
// 调用身份认证接口
const params = {
idCard: formData.value.idCard.toUpperCase(),
phone: formData.value.phone,
code: formData.value.code
};
const result = await $api.createRequest('/app/auth/verify', params, 'post');
// 认证成功
showAuthStatus('success', '身份认证成功');
playTextDirectly('身份认证成功');
// 保存认证信息到store
useUserStore().getUserResume().then(()=>{
setTimeout(() => {
navBack()
}, 500);
})
} catch (error) {
// 认证失败
let errorMsg = '身份认证失败';
if (error.code === 'ID_CARD_ERROR') {
idCardError.value = '身份证号验证失败';
errorMsg = '身份证信息有误';
} else if (error.code === 'PHONE_ERROR') {
phoneError.value = '手机号与身份证不匹配';
errorMsg = '手机号与身份证信息不匹配';
} else if (error.code === 'CODE_ERROR') {
codeError.value = '验证码错误或已过期';
errorMsg = '验证码错误,请重新获取';
}
showAuthStatus('error', errorMsg);
playTextDirectly(errorMsg);
}
};
// 组件卸载时清理定时器
onUnmounted(() => {
if (codeTimer.value) {
clearInterval(codeTimer.value);
}
});
</script>
<style lang="scss" scoped>
.auth-container {
width: 100%;
height: 100%;
padding: 20rpx 40rpx 40rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.auth-header {
text-align: center;
margin-bottom: 60rpx;
}
.auth-title {
font-size: 48rpx;
font-weight: 600;
color: #1677ff;
margin-bottom: 16rpx;
}
.auth-subtitle {
font-size: 28rpx;
color: #666;
}
.auth-form {
flex: 1;
}
.form-item {
margin-bottom: 48rpx;
animation: slideIn 0.3s ease-out;
animation-fill-mode: both;
&:nth-child(1) { animation-delay: 0.1s; }
&:nth-child(2) { animation-delay: 0.2s; }
&:nth-child(3) { animation-delay: 0.3s; }
}
.form-label {
font-size: 28rpx;
color: #333;
margin-bottom: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.error-text {
font-size: 24rpx;
color: #ff4d4f;
flex: 1;
text-align: right;
margin-left: 20rpx;
}
.form-input-wrapper {
position: relative;
height: 96rpx;
border: 2rpx solid #e8e8e8;
border-radius: 16rpx;
background: #fff;
display: flex;
align-items: center;
padding: 0 30rpx;
transition: all 0.3s;
&:focus-within {
border-color: #1677ff;
box-shadow: 0 0 0 2rpx rgba(22, 119, 255, 0.1);
}
}
.form-input {
flex: 1;
height: 100%;
font-size: 32rpx;
color: #333;
&::placeholder {
color: #999;
font-size: 28rpx;
}
}
.code-input {
padding-right: 220rpx;
}
.input-clear {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 20rpx;
}
.send-code-btn {
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
height: 64rpx;
min-width: 120rpx;
padding: 0 20rpx;
background: #1677ff;
border-radius: 12rpx;
font-size: 26rpx;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
&:active {
background: #0958d9;
transform: translateY(-50%) scale(0.98);
}
&.disabled {
background: #d9d9d9;
color: #999;
}
}
.auth-btn-container {
margin-top: 40rpx;
}
.auth-btn {
width: 100%;
height: 100rpx;
background: #1677ff;
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 500;
color: #fff;
border: none;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
&:active {
background: #0958d9;
transform: scale(0.98);
}
&.disabled {
background: #d9d9d9;
color: #999;
}
}
.auth-tips {
font-size: 24rpx;
color: #999;
text-align: center;
margin-top: 24rpx;
line-height: 1.5;
}
.auth-status {
margin-top: 40rpx;
padding: 32rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 20rpx;
&.success {
background: #f6ffed;
border: 1rpx solid #b7eb8f;
}
&.error {
background: #fff2f0;
border: 1rpx solid #ffccc7;
}
&.loading {
background: #f0f5ff;
border: 1rpx solid #adc6ff;
}
}
.status-text {
font-size: 28rpx;
color: #333;
}
/* 动画效果 */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.btnback{
width: 64rpx;
height: 64rpx;
}
</style>
<style lang="stylus" scoped>
.back-button {
position: absolute;
left: 40rpx;
top: 40rpx;
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(0, 0, 0, 0.05);
&:active {
background: rgba(0, 0, 0, 0.1);
}
}
.loading-spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.form-input:focus {
outline: none;
}
.code-input-container {
position: relative;
&:after {
content: '';
position: absolute;
right: 200rpx;
top: 20rpx;
bottom: 20rpx;
width: 1rpx;
background: #e8e8e8;
}
}
.form-input-wrapper.error {
border-color: #ff4d4f;
animation: shake 0.5s;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-10rpx); }
20%, 40%, 60%, 80% { transform: translateX(10rpx); }
}
</style>