Files
mini-programs/src/login_pages/verification/index.tsx
2026-02-14 12:59:21 +08:00

433 lines
13 KiB
TypeScript
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.
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;