index.tsx 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. import { Images } from "@/constants/images";
  2. import { getUserInfo, realNameAuth, UserInfo } from "@/services/user";
  3. import { useFocusEffect, useRouter } from "expo-router";
  4. import React, { useCallback, useState } from "react";
  5. import {
  6. Alert,
  7. ImageBackground,
  8. KeyboardAvoidingView,
  9. Platform,
  10. ScrollView,
  11. StatusBar,
  12. StyleSheet,
  13. Text,
  14. TextInput,
  15. TouchableOpacity,
  16. View,
  17. } from "react-native";
  18. import { useSafeAreaInsets } from "react-native-safe-area-context";
  19. const NAME_REG = /^[\u4e00-\u9fa5]{2,10}$/;
  20. const ID_REG = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
  21. const maskName = (name: string) => {
  22. if (!name) return "";
  23. if (name.length <= 1) return name;
  24. return name[0] + "*".repeat(name.length - 1);
  25. };
  26. const maskIdNum = (id: string) => {
  27. if (!id) return "";
  28. if (id.length <= 8) return id;
  29. return id.slice(0, 3) + "*".repeat(id.length - 7) + id.slice(-4);
  30. };
  31. export default function CertificationScreen() {
  32. const router = useRouter();
  33. const insets = useSafeAreaInsets();
  34. const [user, setUser] = useState<UserInfo | null>(null);
  35. const [idName, setIdName] = useState("");
  36. const [idNum, setIdNum] = useState("");
  37. const [loading, setLoading] = useState(false);
  38. const [submitting, setSubmitting] = useState(false);
  39. const loadUser = useCallback(async () => {
  40. setLoading(true);
  41. try {
  42. const info = await getUserInfo();
  43. setUser(info);
  44. } catch (e) {
  45. console.error("获取用户信息失败:", e);
  46. } finally {
  47. setLoading(false);
  48. }
  49. }, []);
  50. useFocusEffect(
  51. useCallback(() => {
  52. loadUser();
  53. }, [loadUser]),
  54. );
  55. const isVerified = user?.realNameFlag === 1;
  56. const handleBack = () => router.back();
  57. const handleSubmit = async () => {
  58. const trimName = idName.trim();
  59. const trimId = idNum.trim();
  60. if (!trimName) {
  61. Alert.alert("提示", "请输入真实姓名");
  62. return;
  63. }
  64. if (!NAME_REG.test(trimName)) {
  65. Alert.alert("提示", "请输入正确的姓名格式(2-10个中文字符)");
  66. return;
  67. }
  68. if (!trimId) {
  69. Alert.alert("提示", "请输入身份证号");
  70. return;
  71. }
  72. if (!ID_REG.test(trimId)) {
  73. Alert.alert("提示", "请输入正确的身份证号码");
  74. return;
  75. }
  76. try {
  77. setSubmitting(true);
  78. const ok = await realNameAuth(trimName, trimId);
  79. if (ok) {
  80. Alert.alert("提示", "提交成功", [
  81. { text: "确定", onPress: () => router.back() },
  82. ]);
  83. }
  84. } catch (e) {
  85. console.error("提交实名认证失败:", e);
  86. } finally {
  87. setSubmitting(false);
  88. }
  89. };
  90. return (
  91. <ImageBackground
  92. source={{ uri: Images.mine.kaixinMineBg }}
  93. style={styles.container}
  94. resizeMode="cover"
  95. >
  96. <StatusBar barStyle="light-content" />
  97. <View style={[styles.header, { paddingTop: insets.top }]}>
  98. <TouchableOpacity style={styles.backBtn} onPress={handleBack}>
  99. <Text style={styles.backIcon}>‹</Text>
  100. </TouchableOpacity>
  101. <Text style={styles.headerTitle}>实名认证</Text>
  102. <View style={styles.placeholder} />
  103. </View>
  104. <KeyboardAvoidingView
  105. style={{ flex: 1 }}
  106. behavior={Platform.OS === "ios" ? "padding" : undefined}
  107. >
  108. <ScrollView
  109. style={styles.scrollView}
  110. showsVerticalScrollIndicator={false}
  111. keyboardShouldPersistTaps="handled"
  112. >
  113. <View style={styles.content}>
  114. <View style={styles.card}>
  115. {loading ? (
  116. <Text style={styles.loadingText}>加载中...</Text>
  117. ) : isVerified ? (
  118. <>
  119. <View style={styles.verifiedBanner}>
  120. <Text style={styles.verifiedBannerText}>✓ 已完成实名认证</Text>
  121. </View>
  122. <View style={styles.formItem}>
  123. <Text style={styles.label}>真实姓名</Text>
  124. <View style={styles.readonly}>
  125. <Text style={styles.readonlyText}>
  126. {maskName(user?.idName || "")}
  127. </Text>
  128. </View>
  129. </View>
  130. <View style={styles.formItem}>
  131. <Text style={styles.label}>身份证号</Text>
  132. <View style={styles.readonly}>
  133. <Text style={styles.readonlyText}>
  134. {maskIdNum(user?.idNum || "")}
  135. </Text>
  136. </View>
  137. </View>
  138. <View style={styles.tips}>
  139. <Text style={styles.tipsIcon}>ⓘ</Text>
  140. <Text style={styles.tipsText}>
  141. 实名信息已提交,不可修改
  142. </Text>
  143. </View>
  144. </>
  145. ) : (
  146. <>
  147. <View style={styles.formItem}>
  148. <Text style={styles.label}>真实姓名</Text>
  149. <TextInput
  150. style={styles.input}
  151. value={idName}
  152. onChangeText={setIdName}
  153. placeholder="请输入您的真实姓名"
  154. placeholderTextColor="#bbb"
  155. maxLength={20}
  156. />
  157. </View>
  158. <View style={styles.formItem}>
  159. <Text style={styles.label}>身份证号</Text>
  160. <TextInput
  161. style={styles.input}
  162. value={idNum}
  163. onChangeText={setIdNum}
  164. placeholder="请输入您的身份证号码"
  165. placeholderTextColor="#bbb"
  166. maxLength={18}
  167. autoCapitalize="characters"
  168. />
  169. </View>
  170. <View style={styles.tips}>
  171. <Text style={styles.tipsIcon}>⚠️</Text>
  172. <Text style={styles.tipsText}>
  173. 请确保您填写的信息真实有效,提交后将无法修改
  174. </Text>
  175. </View>
  176. <TouchableOpacity
  177. style={[styles.submitBtn, submitting && styles.submitBtnDisabled]}
  178. onPress={handleSubmit}
  179. disabled={submitting}
  180. activeOpacity={0.8}
  181. >
  182. <Text style={styles.submitBtnText}>
  183. {submitting ? "提交中..." : "提交"}
  184. </Text>
  185. </TouchableOpacity>
  186. </>
  187. )}
  188. </View>
  189. </View>
  190. </ScrollView>
  191. </KeyboardAvoidingView>
  192. </ImageBackground>
  193. );
  194. }
  195. const styles = StyleSheet.create({
  196. container: {
  197. flex: 1,
  198. },
  199. header: {
  200. flexDirection: "row",
  201. alignItems: "center",
  202. justifyContent: "space-between",
  203. paddingHorizontal: 10,
  204. height: 80,
  205. },
  206. backBtn: {
  207. width: 40,
  208. height: 40,
  209. justifyContent: "center",
  210. alignItems: "center",
  211. },
  212. backIcon: {
  213. fontSize: 32,
  214. color: "#fff",
  215. fontWeight: "bold",
  216. },
  217. headerTitle: {
  218. fontSize: 16,
  219. fontWeight: "bold",
  220. color: "#fff",
  221. },
  222. placeholder: {
  223. width: 40,
  224. },
  225. scrollView: {
  226. flex: 1,
  227. },
  228. content: {
  229. paddingHorizontal: 15,
  230. paddingTop: 10,
  231. paddingBottom: 30,
  232. },
  233. card: {
  234. backgroundColor: "#fff",
  235. borderRadius: 15,
  236. padding: 20,
  237. },
  238. loadingText: {
  239. textAlign: "center",
  240. color: "#999",
  241. paddingVertical: 40,
  242. },
  243. verifiedBanner: {
  244. backgroundColor: "#e6f9ef",
  245. borderRadius: 8,
  246. paddingVertical: 10,
  247. paddingHorizontal: 14,
  248. marginBottom: 20,
  249. alignItems: "center",
  250. },
  251. verifiedBannerText: {
  252. color: "#1aad5a",
  253. fontSize: 14,
  254. fontWeight: "600",
  255. },
  256. formItem: {
  257. marginBottom: 20,
  258. },
  259. label: {
  260. fontSize: 14,
  261. color: "#333",
  262. fontWeight: "500",
  263. marginBottom: 10,
  264. },
  265. input: {
  266. width: "100%",
  267. height: 40,
  268. paddingHorizontal: 10,
  269. borderWidth: 1,
  270. borderColor: "#e5e5e5",
  271. borderRadius: 5,
  272. fontSize: 14,
  273. color: "#333",
  274. backgroundColor: "#f8f8f8",
  275. },
  276. readonly: {
  277. width: "100%",
  278. height: 40,
  279. paddingHorizontal: 10,
  280. borderWidth: 1,
  281. borderColor: "#e5e5e5",
  282. borderRadius: 5,
  283. backgroundColor: "#f5f5f5",
  284. justifyContent: "center",
  285. },
  286. readonlyText: {
  287. fontSize: 14,
  288. color: "#666",
  289. letterSpacing: 1,
  290. },
  291. tips: {
  292. flexDirection: "row",
  293. alignItems: "flex-start",
  294. padding: 10,
  295. marginBottom: 20,
  296. backgroundColor: "#fff9e6",
  297. borderRadius: 5,
  298. borderLeftWidth: 2,
  299. borderLeftColor: "#ffdd02",
  300. },
  301. tipsIcon: {
  302. fontSize: 14,
  303. marginRight: 6,
  304. },
  305. tipsText: {
  306. flex: 1,
  307. fontSize: 12,
  308. color: "#ff9900",
  309. lineHeight: 18,
  310. },
  311. submitBtn: {
  312. backgroundColor: "#FC7D2E",
  313. borderRadius: 8,
  314. paddingVertical: 13,
  315. alignItems: "center",
  316. },
  317. submitBtnDisabled: {
  318. opacity: 0.6,
  319. },
  320. submitBtnText: {
  321. color: "#fff",
  322. fontSize: 16,
  323. fontWeight: "600",
  324. },
  325. });