diff --git a/src/pages/login/index/index.scss b/src/pages/login/index/index.scss index 6f2f2f0..61cd3c2 100644 --- a/src/pages/login/index/index.scss +++ b/src/pages/login/index/index.scss @@ -496,4 +496,4 @@ cursor: pointer; } } -} \ No newline at end of file +} diff --git a/src/pages/login/index/index.tsx b/src/pages/login/index/index.tsx index 53e7da1..334c69c 100644 --- a/src/pages/login/index/index.tsx +++ b/src/pages/login/index/index.tsx @@ -117,7 +117,7 @@ const LoginPage: React.FC = () => { return ( - { ); }; -export default LoginPage; \ No newline at end of file +export default LoginPage; diff --git a/src/pages/login/verification/index.scss b/src/pages/login/verification/index.scss index 7bf0be3..194e6f7 100644 --- a/src/pages/login/verification/index.scss +++ b/src/pages/login/verification/index.scss @@ -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,16 +268,16 @@ } .char_count { - font-family: 'PingFang SC'; + font-family: "PingFang SC"; font-weight: 400; font-size: 14px; line-height: 1.714; color: rgba(60, 60, 67, 0.3); - + .count_number { color: rgba(60, 60, 67, 0.3); transition: color 0.3s ease; - + &.active { color: #000000; font-weight: 500; @@ -298,19 +298,19 @@ 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; width: 210px; height: 52px; box-sizing: border-box; - + box-shadow: 0px 4px 36px 0px rgba(0, 0, 0, 0.06); .code_input { flex: 1; - font-family: 'PingFang SC'; + font-family: "PingFang SC"; font-weight: 500; font-size: 20px; line-height: 1.6; @@ -318,7 +318,6 @@ border: none; outline: none; background: transparent; - &::placeholder { color: rgba(60, 60, 67, 0.3); @@ -326,16 +325,16 @@ } .char_count { - font-family: 'PingFang SC'; + font-family: "PingFang SC"; font-weight: 400; font-size: 14px; line-height: 1.714; color: rgba(60, 60, 67, 0.3); - + .count_number { color: rgba(60, 60, 67, 0.3); transition: color 0.3s ease; - + &.active { color: #000000; font-weight: 500; @@ -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; + } + } + } +} diff --git a/src/pages/login/verification/index.tsx b/src/pages/login/verification/index.tsx index c8cb116..d18cf50 100644 --- a/src/pages/login/verification/index.tsx +++ b/src/pages/login/verification/index.tsx @@ -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 ( @@ -166,7 +217,6 @@ const VerificationPage: React.FC = () => { - {/* 主要内容 */} {/* 标题区域 */} @@ -190,7 +240,11 @@ const VerificationPage: React.FC = () => { maxlength={11} /> - 0 ? 'count_number active' : 'count_number'}> + 0 ? "count_number active" : "count_number" + } + > {phone.length} /11 @@ -215,19 +269,25 @@ const VerificationPage: React.FC = () => { maxlength={6} /> - 0 ? 'count_number active' : 'count_number'}> + 0 + ? "count_number active" + : "count_number" + } + > {verification_code.length} /6 {/* 调试信息 */} {/* {process.env.NODE_ENV === 'development' && ( @@ -260,14 +322,53 @@ const VerificationPage: React.FC = () => { )} */} - - {/* 底部指示器 */} + + {show_change_mobile_layer && ( + + {/* 底部修改手机号浮层 */} + + + 是否更新已绑定的手机号? + + + + 更新后,原手机号将解除绑定,后续登录和通知将使用新的手机号 + + + + + 当前使用的手机号 + {hidePhone(phone)} + + + 原绑定手机号 + {hidePhone(oldPhone)} + + + + + + + + + )} ); }; -export default VerificationPage; \ No newline at end of file +export default VerificationPage; diff --git a/src/services/loginService.ts b/src/services/loginService.ts index 81efa61..96398b3 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -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) => { 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; + } +};