433 lines
13 KiB
TypeScript
433 lines
13 KiB
TypeScript
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 [display_phone, setDisplayPhone] = useState(""); // 显示带空格的手机号
|
||
const [verification_code, setVerificationCode] = useState(""); // 存储纯数字的验证码
|
||
const [display_code, setDisplayCode] = 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();
|
||
|
||
// 格式化手机号为 3-4-4 格式
|
||
const formatPhone = (value: string): string => {
|
||
// 移除所有非数字字符
|
||
const numbers = value.replace(/\D/g, "");
|
||
|
||
// 按 3-4-4 格式添加空格
|
||
if (numbers.length <= 3) {
|
||
return numbers;
|
||
} else if (numbers.length <= 7) {
|
||
return `${numbers.slice(0, 3)} ${numbers.slice(3)}`;
|
||
} else {
|
||
return `${numbers.slice(0, 3)} ${numbers.slice(3, 7)} ${numbers.slice(7, 11)}`;
|
||
}
|
||
};
|
||
|
||
// 格式化验证码为 3-3 格式
|
||
const formatCode = (value: string): string => {
|
||
// 移除所有非数字字符
|
||
const numbers = value.replace(/\D/g, "");
|
||
|
||
// 按 3-3 格式添加空格
|
||
if (numbers.length <= 3) {
|
||
return numbers;
|
||
} else {
|
||
return `${numbers.slice(0, 3)} ${numbers.slice(3, 6)}`;
|
||
}
|
||
};
|
||
|
||
// 处理手机号输入
|
||
const handlePhoneInput = (e) => {
|
||
const inputValue = e.detail.value;
|
||
// 移除所有非数字字符
|
||
const numbers = inputValue.replace(/\D/g, "");
|
||
// 限制最多11位
|
||
const limitedNumbers = numbers.slice(0, 11);
|
||
|
||
// 保存纯数字版本用于提交
|
||
setPhone(limitedNumbers);
|
||
// 保存格式化版本用于显示
|
||
setDisplayPhone(formatPhone(limitedNumbers));
|
||
};
|
||
|
||
// 处理验证码输入
|
||
const handleCodeInput = (e) => {
|
||
const inputValue = e.detail.value;
|
||
// 移除所有非数字字符
|
||
const numbers = inputValue.replace(/\D/g, "");
|
||
// 限制最多6位
|
||
const limitedNumbers = numbers.slice(0, 6);
|
||
|
||
// 保存纯数字版本用于提交
|
||
setVerificationCode(limitedNumbers);
|
||
// 保存格式化版本用于显示
|
||
setDisplayCode(formatCode(limitedNumbers));
|
||
};
|
||
|
||
// 计算登录按钮是否应该启用
|
||
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,
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!can_send_code) return;
|
||
|
||
try {
|
||
console.log("开始发送验证码,手机号:", phone);
|
||
|
||
// 调用发送短信接口
|
||
const result = await send_sms_code(phone);
|
||
|
||
console.log("发送验证码结果:", result);
|
||
|
||
if (result.success) {
|
||
console.log("验证码发送成功,开始倒计时");
|
||
|
||
Taro.showToast({
|
||
title: "验证码已发送",
|
||
icon: "success",
|
||
duration: 2000,
|
||
});
|
||
|
||
// 开始倒计时
|
||
setCanSendCode(false);
|
||
setCountdown(60);
|
||
|
||
console.log("设置状态: can_send_code = false, countdown = 60");
|
||
|
||
// 发送验证码成功后,让验证码输入框获得焦点并调用系统键盘
|
||
setTimeout(() => {
|
||
// 设置验证码输入框聚焦状态
|
||
setCodeInputFocus(true);
|
||
// 清空验证码,让用户重新输入
|
||
setVerificationCode("");
|
||
setDisplayCode("");
|
||
console.log("设置验证码输入框聚焦");
|
||
}, 500); // 延迟500ms确保Toast显示完成后再聚焦
|
||
} else {
|
||
console.log("验证码发送失败:", result.message);
|
||
Taro.showToast({
|
||
title: result.message || "发送失败",
|
||
icon: "none",
|
||
duration: 2000,
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.warn("发送验证码异常:", error);
|
||
Taro.showToast({
|
||
title: "发送失败,请重试",
|
||
icon: "none",
|
||
duration: 2000,
|
||
});
|
||
}
|
||
};
|
||
|
||
// 倒计时效果
|
||
useEffect(() => {
|
||
console.log("倒计时 useEffect 触发,countdown:", countdown);
|
||
|
||
if (countdown > 0) {
|
||
const timer = setTimeout(() => {
|
||
console.log("倒计时减少,从", countdown, "到", countdown - 1);
|
||
setCountdown(countdown - 1);
|
||
}, 1000);
|
||
return () => clearTimeout(timer);
|
||
} else if (countdown === 0 && !can_send_code) {
|
||
console.log("倒计时结束,重新启用发送按钮");
|
||
setCanSendCode(true);
|
||
}
|
||
}, [countdown, can_send_code]);
|
||
|
||
// 手机号登录
|
||
const handle_phone_login = async () => {
|
||
if (!phone || phone.length !== 11) {
|
||
Taro.showToast({
|
||
title: "请输入正确的手机号",
|
||
icon: "none",
|
||
duration: 2000,
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!verification_code || verification_code.length !== 6) {
|
||
Taro.showToast({
|
||
title: "请输入6位验证码",
|
||
icon: "none",
|
||
duration: 2000,
|
||
});
|
||
return;
|
||
}
|
||
|
||
setIsLoading(true);
|
||
|
||
try {
|
||
// 先进行微信登录获取code
|
||
const login_result = await Taro.login();
|
||
|
||
if (!login_result.code) {
|
||
return {
|
||
success: false,
|
||
message: "微信登录失败",
|
||
};
|
||
}
|
||
|
||
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!);
|
||
if (result.phone_update_status === "existing") {
|
||
const { existing_phone = "" } = result;
|
||
setShowChangeMobileLayer(true);
|
||
setOldPhone(existing_phone);
|
||
return;
|
||
}
|
||
setTimeout(() => {
|
||
Taro.redirectTo({
|
||
url: "/main_pages/index",
|
||
});
|
||
}, 200);
|
||
} else {
|
||
Taro.showToast({
|
||
title: result.message || "登录失败",
|
||
icon: "none",
|
||
duration: 2000,
|
||
});
|
||
}
|
||
} catch (error) {
|
||
Taro.showToast({
|
||
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: "/main_pages/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">
|
||
{/* 背景 */}
|
||
<View className="background">
|
||
<View className="bg_color"></View>
|
||
</View>
|
||
|
||
{/* 主要内容 */}
|
||
<View className="verification_main_content">
|
||
{/* 标题区域 */}
|
||
<View className="title_section">
|
||
<Text className="main_title">手机号注册/登录有场</Text>
|
||
<Text className="sub_title">Go Together, Grow Together</Text>
|
||
</View>
|
||
|
||
{/* 表单区域 */}
|
||
<View className="form_section">
|
||
{/* 手机号输入 */}
|
||
<View className="input_group">
|
||
<View className="input_container">
|
||
<Input
|
||
className="phone_input"
|
||
type="text"
|
||
placeholder="输入中国内地手机号"
|
||
placeholderClass="input_placeholder"
|
||
value={display_phone}
|
||
onInput={handlePhoneInput}
|
||
maxlength={13}
|
||
/>
|
||
<View className="char_count">
|
||
<Text
|
||
className={
|
||
phone.length > 0 ? "count_number active" : "count_number"
|
||
}
|
||
>
|
||
{phone.length}
|
||
</Text>
|
||
/11
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 验证码输入和发送按钮 */}
|
||
<View className="verification_group">
|
||
<View className="input_container">
|
||
<Input
|
||
className="code_input"
|
||
type="text"
|
||
placeholder="输入短信验证码"
|
||
placeholderClass="input_placeholder"
|
||
placeholderStyle="color:#999999;"
|
||
focus={code_input_focus}
|
||
value={display_code}
|
||
onInput={handleCodeInput}
|
||
onFocus={() => setCodeInputFocus(true)}
|
||
onBlur={() => setCodeInputFocus(false)}
|
||
maxlength={7}
|
||
/>
|
||
<View className="char_count">
|
||
<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" : ""}`}
|
||
onClick={handle_send_code}
|
||
disabled={!can_send_code}
|
||
>
|
||
{can_send_code ? (
|
||
"获取验证码"
|
||
) : (
|
||
<View className="countdown_text">
|
||
<Text className="countdown_line1">验证码已发送</Text>
|
||
<Text className="countdown_line2">{countdown}秒后可重发</Text>
|
||
</View>
|
||
)}
|
||
</Button>
|
||
{/* 调试信息 */}
|
||
{/* {process.env.NODE_ENV === 'development' && (
|
||
<View style={{fontSize: '12px', color: '#999', marginTop: '5px'}}>
|
||
调试: can_send_code={can_send_code.toString()}, countdown={countdown}
|
||
</View>
|
||
)} */}
|
||
</View>
|
||
</View>
|
||
|
||
{/* 登录按钮 */}
|
||
<View className="button_section">
|
||
<Button
|
||
className={`login_button ${is_loading ? "loading" : ""} ${
|
||
!can_login ? "disabled" : ""
|
||
}`}
|
||
onClick={handle_phone_login}
|
||
disabled={!can_login}
|
||
>
|
||
{"登录"}
|
||
</Button>
|
||
{/* 调试信息 */}
|
||
{/* {process.env.NODE_ENV === 'development' && (
|
||
<View style={{fontSize: '12px', color: '#999', marginTop: '5px', textAlign: 'center'}}>
|
||
调试: 手机号长度={phone.length}, 验证码长度={verification_code.length}, 可登录={can_login.toString()}
|
||
</View>
|
||
)} */}
|
||
</View>
|
||
</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: "/main_pages/index",
|
||
});
|
||
}}
|
||
>
|
||
{"保持原手机号"}
|
||
</Button>
|
||
<Button className="agree_button" onClick={change_mobile}>
|
||
{"更新为当前手机号"}
|
||
</Button>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)}
|
||
</View>
|
||
);
|
||
};
|
||
|
||
export default VerificationPage;
|