login.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. import { useRouter } from "expo-router";
  2. import React, { useEffect, useRef, useState } from "react";
  3. import {
  4. Alert,
  5. ImageBackground,
  6. KeyboardAvoidingView,
  7. Platform,
  8. ScrollView,
  9. StatusBar,
  10. StyleSheet,
  11. Text,
  12. TextInput,
  13. TouchableOpacity,
  14. View,
  15. } from "react-native";
  16. import { useSafeAreaInsets } from "react-native-safe-area-context";
  17. import { Images } from "@/constants/images";
  18. import { login, sendVerifyCode } from "@/services/user";
  19. export default function LoginScreen() {
  20. const router = useRouter();
  21. const insets = useSafeAreaInsets();
  22. const [phone, setPhone] = useState("");
  23. const [verifyCode, setVerifyCode] = useState("");
  24. const [agreeFlag, setAgreeFlag] = useState(false);
  25. const [countdown, setCountdown] = useState(0);
  26. const [disabled, setDisabled] = useState(false);
  27. const [loading, setLoading] = useState(false);
  28. const [tips, setTips] = useState("获取验证码");
  29. const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
  30. useEffect(() => {
  31. return () => {
  32. if (timerRef.current) {
  33. clearInterval(timerRef.current);
  34. }
  35. };
  36. }, []);
  37. // 验证手机号
  38. const isChinesePhoneNumber = (phoneNumber: string) => {
  39. const phoneNumberPattern = /^1[3-9]\d{9}$/;
  40. return phoneNumberPattern.test(phoneNumber);
  41. };
  42. // 开始倒计时
  43. const startCountdown = () => {
  44. setDisabled(true);
  45. let count = 60;
  46. setTips(`${count}s后重新获取`);
  47. timerRef.current = setInterval(() => {
  48. count--;
  49. if (count > 0) {
  50. setTips(`${count}s后重新获取`);
  51. } else {
  52. resetCountdown();
  53. }
  54. }, 1000);
  55. };
  56. // 重置倒计时
  57. const resetCountdown = () => {
  58. setDisabled(false);
  59. setTips("获取验证码");
  60. if (timerRef.current) {
  61. clearInterval(timerRef.current);
  62. timerRef.current = null;
  63. }
  64. };
  65. // 获取验证码
  66. const handleGetVerifyCode = async () => {
  67. if (disabled) return;
  68. if (phone && isChinesePhoneNumber(phone)) {
  69. try {
  70. const res = await sendVerifyCode(phone, "LOGIN");
  71. if (res) {
  72. Alert.alert("提示", "验证码已发送");
  73. startCountdown();
  74. }
  75. } catch (error) {
  76. Alert.alert("错误", "获取验证码失败,请重试");
  77. }
  78. } else {
  79. Alert.alert("提示", "请输入正确的手机号");
  80. }
  81. };
  82. // 登录
  83. const handleLogin = async () => {
  84. if (!agreeFlag) {
  85. Alert.alert("提示", "请您先阅读并同意用户协议和隐私政策");
  86. return;
  87. }
  88. if (phone && isChinesePhoneNumber(phone) && verifyCode) {
  89. setLoading(true);
  90. try {
  91. const result = await login({
  92. loginWay: "MOBILE",
  93. mobile: phone,
  94. verifycode: verifyCode,
  95. });
  96. if (result.success) {
  97. // TODO: 如果 needInfo 为 true,跳转到完善信息页面
  98. // if (result.needInfo) {
  99. // router.replace('/user-info');
  100. // return;
  101. // }
  102. router.back();
  103. } else {
  104. Alert.alert("错误", "登录失败,请检查验证码");
  105. }
  106. } catch (error) {
  107. Alert.alert("错误", "登录失败");
  108. }
  109. setLoading(false);
  110. } else {
  111. Alert.alert("提示", "请输入手机号和验证码");
  112. }
  113. };
  114. const goBack = () => {
  115. router.back();
  116. };
  117. return (
  118. <View style={styles.container}>
  119. <StatusBar barStyle="light-content" />
  120. <ImageBackground
  121. source={{ uri: Images.common.loginBg }}
  122. style={styles.background}
  123. resizeMode="cover"
  124. >
  125. <KeyboardAvoidingView
  126. style={styles.keyboardView}
  127. behavior={Platform.OS === "ios" ? "padding" : "height"}
  128. >
  129. <ScrollView
  130. contentContainerStyle={styles.scrollContent}
  131. keyboardShouldPersistTaps="handled"
  132. showsVerticalScrollIndicator={false}
  133. >
  134. {/* 底部表单区域 */}
  135. <View
  136. style={[styles.bottom, { paddingBottom: insets.bottom + 20 }]}
  137. >
  138. {/* 表单 */}
  139. <View style={styles.form}>
  140. {/* 手机号输入 */}
  141. <View style={styles.formItem}>
  142. <Text style={styles.label}>手机号</Text>
  143. <TextInput
  144. style={styles.input}
  145. value={phone}
  146. onChangeText={setPhone}
  147. placeholder="手机号"
  148. placeholderTextColor="rgba(255,255,255,0.5)"
  149. keyboardType="phone-pad"
  150. maxLength={11}
  151. />
  152. </View>
  153. <View style={styles.divider} />
  154. {/* 验证码输入 */}
  155. <View style={styles.formItem}>
  156. <Text style={styles.label}>验证码</Text>
  157. <TextInput
  158. style={[styles.input, styles.codeInput]}
  159. value={verifyCode}
  160. onChangeText={setVerifyCode}
  161. placeholder="请填写验证码"
  162. placeholderTextColor="rgba(255,255,255,0.5)"
  163. keyboardType="number-pad"
  164. maxLength={6}
  165. />
  166. <TouchableOpacity
  167. style={[
  168. styles.verifyBtn,
  169. disabled && styles.verifyBtnDisabled,
  170. ]}
  171. onPress={handleGetVerifyCode}
  172. disabled={disabled}
  173. >
  174. <Text
  175. style={[
  176. styles.verifyBtnText,
  177. disabled && styles.verifyBtnTextDisabled,
  178. ]}
  179. >
  180. {tips}
  181. </Text>
  182. </TouchableOpacity>
  183. </View>
  184. <View style={styles.divider} />
  185. </View>
  186. {/* 协议勾选 */}
  187. <View style={styles.agree}>
  188. <TouchableOpacity
  189. style={styles.agreeCheckboxArea}
  190. onPress={() => setAgreeFlag(!agreeFlag)}
  191. activeOpacity={0.8}
  192. >
  193. <View
  194. style={[styles.radio, agreeFlag && styles.radioChecked]}
  195. >
  196. {agreeFlag && <View style={styles.radioInner} />}
  197. </View>
  198. <Text style={styles.agreeText}>我已阅读并同意</Text>
  199. </TouchableOpacity>
  200. <Text
  201. style={styles.linkText}
  202. onPress={() =>
  203. router.push({
  204. pathname: "/agreement",
  205. params: { type: "user.html" },
  206. })
  207. }
  208. >
  209. 《用户协议》
  210. </Text>
  211. <Text style={styles.agreeText}>跟</Text>
  212. <Text
  213. style={styles.linkText}
  214. onPress={() =>
  215. router.push({
  216. pathname: "/agreement",
  217. params: { type: "privacy.html" },
  218. })
  219. }
  220. >
  221. 《隐私政策》
  222. </Text>
  223. </View>
  224. {/* 按钮区域 */}
  225. <View style={styles.btnArea}>
  226. <TouchableOpacity
  227. style={[styles.btn, loading && styles.btnDisabled]}
  228. onPress={handleLogin}
  229. disabled={loading}
  230. activeOpacity={0.8}
  231. >
  232. <ImageBackground
  233. source={{ uri: Images.common.loginBtn }}
  234. style={styles.loginBtnBg}
  235. resizeMode="stretch"
  236. >
  237. <Text style={styles.btnText}>
  238. {loading ? "登录中..." : "登录"}
  239. </Text>
  240. </ImageBackground>
  241. </TouchableOpacity>
  242. <TouchableOpacity style={styles.btnBack} onPress={goBack}>
  243. <Text style={styles.btnBackText}>返回</Text>
  244. </TouchableOpacity>
  245. </View>
  246. </View>
  247. </ScrollView>
  248. </KeyboardAvoidingView>
  249. </ImageBackground>
  250. </View>
  251. );
  252. }
  253. const styles = StyleSheet.create({
  254. container: {
  255. flex: 1,
  256. backgroundColor: "#1a1a2e",
  257. },
  258. background: {
  259. flex: 1,
  260. },
  261. keyboardView: {
  262. flex: 1,
  263. },
  264. scrollContent: {
  265. flexGrow: 1,
  266. justifyContent: "flex-end",
  267. },
  268. bottom: {
  269. width: "100%",
  270. },
  271. form: {
  272. paddingHorizontal: 25,
  273. },
  274. formItem: {
  275. flexDirection: "row",
  276. alignItems: "center",
  277. paddingVertical: 12,
  278. },
  279. label: {
  280. color: "#fff",
  281. fontSize: 14,
  282. width: 50,
  283. },
  284. input: {
  285. flex: 1,
  286. color: "#fff",
  287. fontSize: 14,
  288. paddingVertical: 8,
  289. outlineStyle: "none",
  290. } as any,
  291. codeInput: {
  292. flex: 1,
  293. },
  294. divider: {
  295. height: 1,
  296. backgroundColor: "rgba(255,255,255,0.2)",
  297. },
  298. verifyBtn: {
  299. backgroundColor: "#000",
  300. borderRadius: 4,
  301. paddingHorizontal: 10,
  302. paddingVertical: 5,
  303. minWidth: 80,
  304. alignItems: "center",
  305. },
  306. verifyBtnDisabled: {
  307. backgroundColor: "#ccc",
  308. },
  309. verifyBtnText: {
  310. color: "#fff",
  311. fontSize: 12,
  312. },
  313. verifyBtnTextDisabled: {
  314. color: "#666",
  315. },
  316. agreeCheckboxArea: {
  317. flexDirection: "row",
  318. alignItems: "center",
  319. },
  320. agree: {
  321. flexDirection: "row",
  322. alignItems: "center",
  323. justifyContent: "center",
  324. marginTop: 25,
  325. paddingHorizontal: 25,
  326. },
  327. radio: {
  328. width: 16,
  329. height: 16,
  330. borderRadius: 8,
  331. borderWidth: 1,
  332. borderColor: "rgba(255,255,255,0.5)",
  333. marginRight: 6,
  334. justifyContent: "center",
  335. alignItems: "center",
  336. },
  337. radioChecked: {
  338. borderColor: "#8b3dff",
  339. backgroundColor: "#8b3dff",
  340. },
  341. radioInner: {
  342. width: 6,
  343. height: 6,
  344. borderRadius: 3,
  345. backgroundColor: "#fff",
  346. },
  347. agreeText: {
  348. color: "#fff",
  349. fontSize: 12,
  350. },
  351. linkText: {
  352. color: "#8b3dff",
  353. },
  354. btnArea: {
  355. paddingTop: 15,
  356. alignItems: "center",
  357. },
  358. btn: {
  359. width: 234,
  360. height: 45,
  361. overflow: "hidden",
  362. },
  363. loginBtnBg: {
  364. width: "100%",
  365. height: "100%",
  366. justifyContent: "center",
  367. alignItems: "center",
  368. },
  369. btnDisabled: {
  370. opacity: 0.6,
  371. },
  372. btnText: {
  373. color: "#fff",
  374. fontSize: 14,
  375. fontWeight: "600",
  376. },
  377. btnBack: {
  378. width: 234,
  379. height: 35,
  380. backgroundColor: "#fff",
  381. borderRadius: 25,
  382. justifyContent: "center",
  383. alignItems: "center",
  384. marginTop: 10,
  385. borderWidth: 1,
  386. borderColor: "#fff",
  387. },
  388. btnBackText: {
  389. color: "#888",
  390. fontSize: 12,
  391. },
  392. });