校验并修改用户手机号

This commit is contained in:
2025-09-11 16:23:07 +08:00
parent 1fb6d6ee65
commit c430ed407b
5 changed files with 308 additions and 100 deletions

View File

@@ -1,7 +1,7 @@
// 验证码页面样式
.verification_page {
min-height: 100vh;
background: #FAFAFA;
background: #fafafa;
position: relative;
overflow: hidden;
box-sizing: border-box;
@@ -19,7 +19,7 @@
.bg_color {
width: 100%;
height: 100%;
background: #FAFAFA;
background: #fafafa;
}
}
@@ -34,7 +34,7 @@
height: 54px;
.time {
font-family: 'SF Pro';
font-family: "SF Pro";
font-weight: 590;
font-size: 17px;
line-height: 1.294;
@@ -68,7 +68,7 @@
position: relative;
&::before {
content: '';
content: "";
position: absolute;
right: -1.33px;
top: 4.78px;
@@ -100,7 +100,7 @@
display: flex;
align-items: center;
justify-content: center;
background: #FFFFFF;
background: #ffffff;
border-radius: 50%;
cursor: pointer;
@@ -124,7 +124,7 @@
width: 30px;
height: 30px;
background: rgba(255, 255, 255, 0.7);
border: 0.35px solid #DEDEDE;
border: 0.35px solid #dedede;
border-radius: 15px;
display: flex;
align-items: center;
@@ -141,7 +141,7 @@
&::before,
&::after {
content: '';
content: "";
position: absolute;
width: 4px;
height: 4px;
@@ -170,7 +170,7 @@
position: relative;
&::before {
content: '';
content: "";
position: absolute;
top: 1.5px;
left: 1.5px;
@@ -181,7 +181,7 @@
}
&::after {
content: '';
content: "";
position: absolute;
top: 7px;
left: 7px;
@@ -213,10 +213,10 @@
flex-direction: column;
gap: 8px;
text-align: left;
padding: 12px 24px 36px 24px ;
padding: 12px 24px 36px 24px;
.main_title {
font-family: 'PingFang SC';
font-family: "PingFang SC";
font-weight: 600;
font-size: 28px;
line-height: 1.4;
@@ -224,7 +224,7 @@
}
.sub_title {
font-family: 'PingFang SC';
font-family: "PingFang SC";
font-weight: 300;
font-size: 18px;
line-height: 1.4;
@@ -245,7 +245,7 @@
display: flex;
justify-content: space-between;
align-items: center;
background: #FFFFFF;
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 12px;
padding: 10px 12px;
@@ -253,7 +253,7 @@
.phone_input {
flex: 1;
font-family: 'PingFang SC';
font-family: "PingFang SC";
font-weight: 500;
font-size: 20px;
line-height: 1.6;
@@ -268,7 +268,7 @@
}
.char_count {
font-family: 'PingFang SC';
font-family: "PingFang SC";
font-weight: 400;
font-size: 14px;
line-height: 1.714;
@@ -298,7 +298,7 @@
display: flex;
justify-content: space-between;
align-items: center;
background: #FFFFFF;
background: #ffffff;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 12px;
padding: 10px 12px;
@@ -310,7 +310,7 @@
.code_input {
flex: 1;
font-family: 'PingFang SC';
font-family: "PingFang SC";
font-weight: 500;
font-size: 20px;
line-height: 1.6;
@@ -319,14 +319,13 @@
outline: none;
background: transparent;
&::placeholder {
color: rgba(60, 60, 67, 0.3);
}
}
.char_count {
font-family: 'PingFang SC';
font-family: "PingFang SC";
font-weight: 400;
font-size: 14px;
line-height: 1.714;
@@ -353,11 +352,11 @@
background: #000000;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 16px;
font-family: 'PingFang SC';
font-family: "PingFang SC";
font-weight: 600;
font-size: 16px;
line-height: 1.4;
color: #FFFFFF;
color: #ffffff;
box-shadow: 0px 8px 64px 0px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(32px);
cursor: pointer;
@@ -378,15 +377,15 @@
gap: 2px;
.countdown_line1 {
font-family: 'PingFang SC';
font-family: "PingFang SC";
font-weight: 500;
font-size: 12px;
line-height: 1.2;
color: #FFFFFF;
color: #ffffff;
}
.countdown_line2 {
font-family: 'PingFang SC';
font-family: "PingFang SC";
font-weight: 400;
font-size: 11px;
line-height: 1.2;
@@ -396,8 +395,6 @@
}
}
// 登录按钮
.login_button {
width: 100%;
@@ -405,11 +402,11 @@
background: #000000;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 16px;
font-family: 'PingFang SC';
font-family: "PingFang SC";
font-weight: 600;
font-size: 16px;
padding: 6px 2px;
color: #FFFFFF;
color: #ffffff;
box-shadow: 0px 8px 64px 0px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(32px);
cursor: pointer;
@@ -419,7 +416,6 @@
opacity: 0.7;
cursor: not-allowed;
}
}
// 协议区域
@@ -429,14 +425,14 @@
line-height: 1.5;
.terms_text {
font-family: 'PingFang SC';
font-family: "PingFang SC";
font-weight: 400;
font-size: 14px;
color: rgba(60, 60, 67, 0.6);
}
.terms_link {
font-family: 'PingFang SC';
font-family: "PingFang SC";
font-weight: 500;
font-size: 14px;
color: #000000;
@@ -463,3 +459,94 @@
z-index: 10;
}
// 浮层遮罩
.change_mobile_overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 100;
display: flex;
align-items: flex-end;
// 底部条款浮层
.change_mobile_float_layer {
position: relative;
bottom: 0;
left: 0;
right: 0;
background: #ffffff;
border-radius: 24px 24px 0 0;
padding: 24px 24px 34px;
z-index: 101;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
width: 100%;
display: flex;
flex-direction: column;
font-family: "PingFang SC";
// 浮层标题
.float_title {
margin-bottom: 20px;
.title_text {
font-weight: 600;
font-size: 22px;
color: #000000;
line-height: 1.4;
}
}
.hint_content {
margin-bottom: 20px;
.hint_text {
font-weight: 400;
font-size: 14px;
color: #000000;
line-height: 1.4;
}
}
.bind_mobile_info_box {
border-radius: 16px;
border: 1px solid rgba(0, 0, 0, 0.06);
margin-bottom: 20px;
.bind_item {
display: flex;
justify-content: space-between;
align-items: center;
height: 44px;
color: #3c3c4399;
padding: 0 16px;
& + .bind_item {
border-top: 1px solid rgba(0, 0, 0, 0.06);
}
.mobile {
color: #000000;
}
}
}
.button_group {
display: flex;
justify-content: space-between;
gap: 8px;
min-height: 0;
Button {
border: none;
border-radius: 16px;
flex: 1;
font-size: 16px;
height: 50px;
line-height: 50px;
}
.cancel_button {
background-color: #ffffff;
color: #000000;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.agree_button {
background: #000000;
color: #ffffff;
}
}
}
}

View File

@@ -1,30 +1,39 @@
import React, { useState, useEffect } from 'react';
import { View, Text, Input, Button, Image } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
import { phone_auth_login, send_sms_code, save_login_state } from '../../../services/loginService';
import './index.scss';
import React, { useState, useEffect } from "react";
import { View, Text, Input, Button, Image } from "@tarojs/components";
import Taro, { useRouter } from "@tarojs/taro";
import {
phone_auth_login,
send_sms_code,
save_login_state,
updateUserPhone,
} from "../../../services/loginService";
import "./index.scss";
const VerificationPage: React.FC = () => {
const [phone, setPhone] = useState('');
const [verification_code, setVerificationCode] = useState('');
const [phone, setPhone] = useState("");
const [verification_code, setVerificationCode] = useState("");
const [countdown, setCountdown] = useState(0);
const [can_send_code, setCanSendCode] = useState(true);
const [is_loading, setIsLoading] = useState(false);
const [code_input_focus, setCodeInputFocus] = useState(false);
const [show_change_mobile_layer, setShowChangeMobileLayer] = useState(false);
const [oldPhone, setOldPhone] = useState("");
const { params: { redirect } } = useRouter();
const {
params: { redirect },
} = useRouter();
// 计算登录按钮是否应该启用
const can_login = phone.length === 11 && verification_code.length === 6 && !is_loading;
const can_login =
phone.length === 11 && verification_code.length === 6 && !is_loading;
// 发送验证码
const handle_send_code = async () => {
if (!phone || phone.length !== 11) {
Taro.showToast({
title: '请输入正确的手机号',
icon: 'none',
duration: 2000
title: "请输入正确的手机号",
icon: "none",
duration: 2000,
});
return;
}
@@ -32,66 +41,66 @@ const VerificationPage: React.FC = () => {
if (!can_send_code) return;
try {
console.log('开始发送验证码,手机号:', phone);
console.log("开始发送验证码,手机号:", phone);
// 调用发送短信接口
const result = await send_sms_code(phone);
console.log('发送验证码结果:', result);
console.log("发送验证码结果:", result);
if (result.success) {
console.log('验证码发送成功,开始倒计时');
console.log("验证码发送成功,开始倒计时");
Taro.showToast({
title: '验证码已发送',
icon: 'success',
duration: 2000
title: "验证码已发送",
icon: "success",
duration: 2000,
});
// 开始倒计时
setCanSendCode(false);
setCountdown(60);
console.log('设置状态: can_send_code = false, countdown = 60');
console.log("设置状态: can_send_code = false, countdown = 60");
// 发送验证码成功后,让验证码输入框获得焦点并调用系统键盘
setTimeout(() => {
// 设置验证码输入框聚焦状态
setCodeInputFocus(true);
// 清空验证码,让用户重新输入
setVerificationCode('');
console.log('设置验证码输入框聚焦');
setVerificationCode("");
console.log("设置验证码输入框聚焦");
}, 500); // 延迟500ms确保Toast显示完成后再聚焦
} else {
console.log('验证码发送失败:', result.message);
console.log("验证码发送失败:", result.message);
Taro.showToast({
title: result.message || '发送失败',
icon: 'none',
duration: 2000
title: result.message || "发送失败",
icon: "none",
duration: 2000,
});
}
} catch (error) {
console.error('发送验证码异常:', error);
console.error("发送验证码异常:", error);
Taro.showToast({
title: '发送失败,请重试',
icon: 'none',
duration: 2000
title: "发送失败,请重试",
icon: "none",
duration: 2000,
});
}
};
// 倒计时效果
useEffect(() => {
console.log('倒计时 useEffect 触发countdown:', countdown);
console.log("倒计时 useEffect 触发countdown:", countdown);
if (countdown > 0) {
const timer = setTimeout(() => {
console.log('倒计时减少,从', countdown, '到', countdown - 1);
console.log("倒计时减少,从", countdown, "到", countdown - 1);
setCountdown(countdown - 1);
}, 1000);
return () => clearTimeout(timer);
} else if (countdown === 0 && !can_send_code) {
console.log('倒计时结束,重新启用发送按钮');
console.log("倒计时结束,重新启用发送按钮");
setCanSendCode(true);
}
}, [countdown, can_send_code]);
@@ -100,18 +109,18 @@ const VerificationPage: React.FC = () => {
const handle_phone_login = async () => {
if (!phone || phone.length !== 11) {
Taro.showToast({
title: '请输入正确的手机号',
icon: 'none',
duration: 2000
title: "请输入正确的手机号",
icon: "none",
duration: 2000,
});
return;
}
if (!verification_code || verification_code.length !== 6) {
Taro.showToast({
title: '请输入6位验证码',
icon: 'none',
duration: 2000
title: "请输入6位验证码",
icon: "none",
duration: 2000,
});
return;
}
@@ -119,45 +128,87 @@ const VerificationPage: React.FC = () => {
setIsLoading(true);
try {
// 先进行微信登录获取code
const login_result = await Taro.login();
if (!login_result.code) {
return {
success: false,
message: '微信登录失败'
message: "微信登录失败",
};
}
const result = await phone_auth_login({ phone, verification_code, user_code: login_result.code });
const result = await phone_auth_login({
phone,
verification_code,
user_code: login_result.code,
});
if (result.success) {
save_login_state(result.token!, result.user_info!)
save_login_state(result.token!, result.user_info!);
if (result.phone_update_status === "existing") {
const { existing_phone = "" } = result;
setShowChangeMobileLayer(true);
setOldPhone(existing_phone);
return;
}
setTimeout(() => {
Taro.redirectTo({
url: '/pages/list/index'
url: "/pages/list/index",
});
}, 200);
} else {
Taro.showToast({
title: result.message || '登录失败',
icon: 'none',
duration: 2000
title: result.message || "登录失败",
icon: "none",
duration: 2000,
});
}
} catch (error) {
Taro.showToast({
title: '登录失败,请重试',
icon: 'none',
duration: 2000
title: "登录失败,请重试",
icon: "none",
duration: 2000,
});
} finally {
setIsLoading(false);
}
};
const change_mobile = async () => {
setIsLoading(true);
try {
const res = await updateUserPhone({ phone });
if (res.code === 0) {
setTimeout(() => {
Taro.redirectTo({
url: "/pages/list/index",
});
}, 200);
} else {
Taro.showToast({
title: res.message || "更新手机号失败",
icon: "none",
duration: 2000,
});
}
} catch (e) {
Taro.showToast({
title: "更新失败,请重试",
icon: "none",
duration: 2000,
});
} finally {
setIsLoading(false);
}
};
const hidePhone = (phone: string): string => {
if (!phone) {
return "*";
}
return phone.replace(/(\d{3})(\d*)(\d{4})/,'$1****$3');
};
return (
<View className="verification_page">
@@ -166,7 +217,6 @@ const VerificationPage: React.FC = () => {
<View className="bg_color"></View>
</View>
{/* 主要内容 */}
<View className="main_content">
{/* 标题区域 */}
@@ -190,7 +240,11 @@ const VerificationPage: React.FC = () => {
maxlength={11}
/>
<View className="char_count">
<Text className={phone.length > 0 ? 'count_number active' : 'count_number'}>
<Text
className={
phone.length > 0 ? "count_number active" : "count_number"
}
>
{phone.length}
</Text>
/11
@@ -215,19 +269,25 @@ const VerificationPage: React.FC = () => {
maxlength={6}
/>
<View className="char_count">
<Text className={verification_code.length > 0 ? 'count_number active' : 'count_number'}>
<Text
className={
verification_code.length > 0
? "count_number active"
: "count_number"
}
>
{verification_code.length}
</Text>
/6
</View>
</View>
<Button
className={`send_code_button ${!can_send_code ? 'disabled' : ''}`}
className={`send_code_button ${!can_send_code ? "disabled" : ""}`}
onClick={handle_send_code}
disabled={!can_send_code}
>
{can_send_code ? (
'获取验证码'
"获取验证码"
) : (
<View className="countdown_text">
<Text className="countdown_line1"></Text>
@@ -247,11 +307,13 @@ const VerificationPage: React.FC = () => {
{/* 登录按钮 */}
<View className="button_section">
<Button
className={`login_button ${is_loading ? 'loading' : ''} ${!can_login ? 'disabled' : ''}`}
className={`login_button ${is_loading ? "loading" : ""} ${
!can_login ? "disabled" : ""
}`}
onClick={handle_phone_login}
disabled={!can_login}
>
{'登录'}
{"登录"}
</Button>
{/* 调试信息 */}
{/* {process.env.NODE_ENV === 'development' && (
@@ -260,12 +322,51 @@ const VerificationPage: React.FC = () => {
</View>
)} */}
</View>
</View>
{/* 底部指示器 */}
<View className="home_indicator"></View>
{show_change_mobile_layer && (
<View className="change_mobile_overlay">
{/* 底部修改手机号浮层 */}
<View className="change_mobile_float_layer">
<View className="float_title">
<Text className="title_text"></Text>
</View>
<View className="hint_content">
<Text className="hint_text">
使
</Text>
</View>
<View className="bind_mobile_info_box">
<View className="bind_item">
<Text>使</Text>
<Text className="mobile">{hidePhone(phone)}</Text>
</View>
<View className="bind_item">
<Text></Text>
<Text className="mobile">{hidePhone(oldPhone)}</Text>
</View>
</View>
<View className="button_group">
<Button
className="cancel_button"
onClick={() => {
Taro.redirectTo({
url: "/pages/list/index",
});
}}
>
{"保持原手机号"}
</Button>
<Button className="agree_button" onClick={change_mobile}>
{"更新为当前手机号"}
</Button>
</View>
</View>
</View>
)}
</View>
);
};

View File

@@ -20,6 +20,8 @@ export interface LoginResponse {
message: string;
token?: string;
user_info?: WechatUserInfo;
phone_update_status?: string;
existing_phone?: string;
}
// 发送短信响应接口
@@ -134,6 +136,11 @@ export interface PhoneLoginParams {
user_code: string;
}
// 更新手机号接口参数
export interface ChangePhoneParams {
phone: string;
}
// 手机号验证码登录
export const phone_auth_login = async (
params: PhoneLoginParams,
@@ -159,6 +166,8 @@ export const phone_auth_login = async (
message: "登录成功",
token: verify_response.data?.token,
user_info: verify_response.data?.userInfo,
phone_update_status: verify_response.data?.phone_update_status,
existing_phone: verify_response.data?.existing_phone,
};
} else {
return {
@@ -391,3 +400,14 @@ export const updateUserProfile = async (payload: Partial<UserInfoType>) => {
throw error;
}
};
// 更新用户手机号
export const updateUserPhone = async (payload: ChangePhoneParams) => {
try {
const response = await httpService.post("/user/update_phone", payload);
return response;
} catch (error) {
console.error("更新用户手机号失败:", error);
throw error;
}
};