import UIButton from "@/components/ui/UIButton"; import { Colors } from "@/constants/theme"; import api from "@/utils/api"; import { DatePicker, Input, Modal, Radio, Toast } from "@ant-design/react-native"; import { Ionicons } from "@expo/vector-icons"; import { usePreventRemove, useRoute } from "@react-navigation/native"; import { NavigationAction } from "@react-navigation/routers"; import { Link, Stack, useNavigation } from "expo-router"; import React, { useCallback, useRef, useState } from "react"; import { KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { UploadScreen } from "../credit/upload"; type CustomerForm = { name: string; mobile: string; gender: string; birthday?: Date; idcard: string; nation: string; registered: string; residential: string; photo: string; occupation: string; other: string; }; type FieldKey = "name" | "mobile" | "idcard"; type FieldErrors = Partial>; type BirthDateSource = "unset" | "manual" | "id-card"; type Option = { label: string; value: string; }; const GENDER_OPTIONS: Option[] = [ { label: "男", value: "1" }, { label: "女", value: "2" }, { label: "其它", value: "3" } ]; const MOBILE_PATTERN = /^1\d{10}$/; const IDCARD_PATTERN = /^(?:\d{15}|\d{17}[\dX])$/; const EMPTY_CUSTOMER_FORM: CustomerForm = { name: "", mobile: "", gender: "", birthday: undefined, idcard: "", nation: "", registered: "", residential: "", photo: "", occupation: "", other: "", }; type CustomerExtPayload = { nation: string; registered: string | null; residential: string | null; photo: string | null; occupation: string | null; other: string | null; }; type CustomerPayload = { name: string; mobile: string; gender: number; birthday: string | null; idcard: string; ext: CustomerExtPayload; }; function sanitizePhone(value: string) { return value.replace(/\D/g, "").slice(0, 11); } function sanitizeIdCard(value: string) { return value.replace(/[^0-9xX]/g, "").toUpperCase().slice(0, 18); } function padZero(value: number) { return String(value).padStart(2, "0"); } function formatDate(date?: Date) { if (!date) { return ""; } return `${date.getFullYear()}-${padZero(date.getMonth() + 1)}-${padZero(date.getDate())}`; } function isValidDatePart(year: number, month: number, day: number) { if (year < 1900 || year > new Date().getFullYear()) { return false; } const date = new Date(year, month - 1, day); return ( date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day && date.getTime() <= Date.now() ); } function inferBirthDateFromIdCard(idCard: string) { if (idCard.length < 14) { return undefined; } const birthText = idCard.slice(6, 14); if (!/^\d{8}$/.test(birthText)) { return undefined; } const year = Number(birthText.slice(0, 4)); const month = Number(birthText.slice(4, 6)); const day = Number(birthText.slice(6, 8)); if (!isValidDatePart(year, month, day)) { return undefined; } return new Date(year, month - 1, day); } function nullableText(value: string) { const trimmed = value.trim(); return trimmed || null; } function validateForm(form: CustomerForm): FieldErrors { const nextErrors: FieldErrors = {}; const mobile = form.mobile.trim(); const idcard = form.idcard.trim(); if (!form.name.trim()) { nextErrors.name = "请输入客户姓名"; } if (!mobile && !idcard) { nextErrors.mobile = "手机号和身份证号至少填写一项"; nextErrors.idcard = "手机号和身份证号至少填写一项"; return nextErrors; } if (mobile && !MOBILE_PATTERN.test(mobile)) { nextErrors.mobile = "请输入 11 位手机号"; } if (idcard && !IDCARD_PATTERN.test(idcard)) { nextErrors.idcard = "请输入正确的身份证号"; } return nextErrors; } function buildCustomerPayload(form: CustomerForm): CustomerPayload { return { name: form.name.trim(), mobile: form.mobile.trim(), gender: Number(form.gender) || 0, birthday: form.birthday ? formatDate(form.birthday) : null, idcard: form.idcard.trim(), ext: { nation: form.nation.trim(), registered: nullableText(form.registered), residential: nullableText(form.residential), photo: nullableText(form.photo), occupation: nullableText(form.occupation), other: nullableText(form.other), }, }; } function FieldLabel({ label, required = false, }: { label: string; required?: boolean; }) { return ( {label} {required ? * : null} ); } function FieldMessage({ error, helper, }: { error?: string; helper?: string; }) { if (error) { return {error}; } if (helper) { return {helper}; } return null; } function PickerField({ label, required = false, value, placeholder, error, helper, disabled = false, onPress, }: { label: string; required?: boolean; value?: string; placeholder: string; error?: string; helper?: string; disabled?: boolean; onPress: () => void; }) { const hasValue = Boolean(value); return ( ({ opacity: disabled ? 0.55 : pressed ? 0.9 : 1, })} > {hasValue ? value : placeholder} ); } export default function AddCustomerScreen() { const insets = useSafeAreaInsets(); const navigation = useNavigation(); const [form, setForm] = useState(EMPTY_CUSTOMER_FORM); const [errors, setErrors] = useState({}); const [birthDateSource, setBirthDateSource] = useState("unset"); const [saving, setSaving] = useState(false); const leaveConfirmVisibleRef = useRef(false); const savedLeaveRef = useRef(false); const route = useRoute(); const scrollViewRef = useRef(null!); const hasDraft = Boolean( form.name.trim() || form.mobile.trim() || form.gender || form.birthday || form.idcard.trim() || form.nation.trim() || form.registered.trim() || form.residential.trim() || form.photo.trim() || form.occupation.trim() || form.other.trim() ); const openLeaveConfirm = useCallback( (action: NavigationAction) => { if (leaveConfirmVisibleRef.current) { return; } leaveConfirmVisibleRef.current = true; Modal.alert( "放弃当前编辑", "当前页面还有未保存的客户资料,确定要返回吗?", [ { text: "继续编辑", style: "cancel", onPress: () => { leaveConfirmVisibleRef.current = false; }, }, { text: "确认返回", style: "destructive", onPress: () => { leaveConfirmVisibleRef.current = false; navigation.dispatch(action); }, }, ], () => { leaveConfirmVisibleRef.current = false; return true; } ); }, [navigation] ); usePreventRemove(hasDraft, ({ data }) => { if (savedLeaveRef.current) { navigation.dispatch(data.action); return; } openLeaveConfirm(data.action); }); const clearErrors = (keys: FieldKey[]) => { setErrors((prev) => { const next = { ...prev }; keys.forEach((key) => { delete next[key]; }); return next; }); }; const handleNameChange = (value: string) => { setForm((prev) => ({ ...prev, name: value })); clearErrors(["name"]); }; const handleMobileChange = (value: string) => { setForm((prev) => ({ ...prev, mobile: sanitizePhone(value) })); clearErrors(["mobile", "idcard"]); }; const handleIdCardChange = (value: string) => { const nextIdCard = sanitizeIdCard(value); const inferredBirthDate = inferBirthDateFromIdCard(nextIdCard); const canAutofillBirthDate = birthDateSource !== "manual" || !form.birthday; setForm((prev) => ({ ...prev, idcard: nextIdCard, birthday: inferredBirthDate && canAutofillBirthDate ? inferredBirthDate : !inferredBirthDate && birthDateSource === "id-card" ? undefined : prev.birthday, })); if (inferredBirthDate && canAutofillBirthDate) { setBirthDateSource("id-card"); } else if (!inferredBirthDate && birthDateSource === "id-card") { setBirthDateSource("unset"); } clearErrors(["mobile", "idcard"]); }; const savedRef = useRef(false); const onSelectCredis = (isCancel: boolean) => { setSelectCredit(false); // if (isCancel) { // return; // } if (savedRef.current || !isCancel) { navigation.goBack(); return; } } const handleSubmit = async () => { if (saving) { return; } const nextErrors = validateForm(form); setErrors(nextErrors); const firstError = Object.values(nextErrors)[0]; if (firstError) { Toast.fail(firstError); scrollViewRef.current?.scrollTo({ y: 160, animated: true }); return; } setSaving(true); const toastKey = Toast.loading("正在保存客户..."); try { const customerId = await api.post("/customer/add", buildCustomerPayload(form)); Toast.success("客户已保存"); savedLeaveRef.current = true; // setForm(EMPTY_CUSTOMER_FORM); // setBirthDateSource("unset"); // setErrors({}); // @ts-ignore if (route.params?.onGoBack) { // @ts-ignore route.params.onGoBack({ customerId }); navigation.goBack(); } else { Modal.alert("客户添加完成", "是否立即上传征信信息", [{ text: '否', }, { 'text': '立即上传', onPress: () => { setSelectCredit(true); } }]) } } catch (error) { console.error("保存客户失败:", error); Toast.fail(error instanceof Error ? error.message : "保存客户失败,请稍后重试"); } finally { setSaving(false); Toast.remove(toastKey); } }; const birthDateHelper = birthDateSource === "id-card" ? "已根据身份证自动识别,可继续手动调整" : "填写身份证后可自动识别,也可以手动选择"; const [selectCredit, setSelectCredit] = useState(false); return ( 新建客户 提示: 您可以先上传分析征信,然后从已分析征信中选择一条信息以填充客户资料。 { const selected = event.target.value; if (typeof selected !== "string" && typeof selected !== "number") { return; } setForm((prev) => ({ ...prev, gender: String(selected), })); }} style={styles.radioGroup} > {GENDER_OPTIONS.map((option) => ( {option.label} ))} { setForm((prev) => ({ ...prev, birthday: value })); setBirthDateSource("manual"); }} > {({ disabled, toggle }) => ( )} setForm((prev) => ({ ...prev, nation: value }))} placeholder="请输入民族" allowClear maxLength={16} style={styles.inputContainer} inputStyle={styles.inputText} /> setForm((prev) => ({ ...prev, registered: value }))} placeholder="请输入户籍所在地" allowClear style={styles.inputContainer} inputStyle={styles.inputText} /> setForm((prev) => ({ ...prev, residential: value }))} placeholder="请输入现居住地" allowClear style={styles.inputContainer} inputStyle={styles.inputText} /> setForm((prev) => ({ ...prev, photo: value }))} placeholder="请输入照片地址" allowClear style={styles.inputContainer} inputStyle={styles.inputText} /> setForm((prev) => ({ ...prev, occupation: value }))} placeholder="请输入职业" allowClear style={styles.inputContainer} inputStyle={styles.inputText} /> setForm((prev) => ({ ...prev, other: value }))} placeholder="补充备注、渠道来源、客户标签等" autoSize={{ minRows: 4, maxRows: 6 }} maxLength={200} showCount allowClear style={styles.textAreaContainer} inputStyle={styles.textAreaText} /> 保存客户 ); } const styles = StyleSheet.create({ inputContainer: { minHeight: 54, borderRadius: 16, borderWidth: 1, borderColor: Colors.border_color_base, backgroundColor: Colors.fill_body, paddingHorizontal: 14, }, inputContainerError: { borderColor: "#fca5a5", backgroundColor: "#fef2f2", }, inputText: { color: Colors.color_text_base, fontSize: 16, paddingVertical: Platform.OS === "web" ? 12 : 10, }, radioGroup: { flexDirection: "row", gap: 24, minHeight: 44, alignItems: "center", }, textAreaContainer: { borderRadius: 16, borderWidth: 1, borderColor: Colors.border_color_base, backgroundColor: Colors.fill_body, paddingHorizontal: 14, paddingTop: 12, paddingBottom: 12, }, textAreaText: { color: Colors.color_text_base, fontSize: 16, lineHeight: 22, minHeight: 96, textAlignVertical: "top", }, });