|
|
@@ -1,29 +1,631 @@
|
|
|
-import { Button } from "@ant-design/react-native";
|
|
|
-import { KeyboardAvoidingView, Platform, ScrollView, View } from "react-native";
|
|
|
+import UIButton from "@/components/ui/UIButton";
|
|
|
+import { Colors } from "@/constants/theme";
|
|
|
+import { DatePicker, Input, Modal, Picker, Toast } from "@ant-design/react-native";
|
|
|
+import { Ionicons } from "@expo/vector-icons";
|
|
|
+import { usePreventRemove } 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,
|
|
|
+ View,
|
|
|
+} from "react-native";
|
|
|
+import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
|
+
|
|
|
+type CustomerForm = {
|
|
|
+ name: string;
|
|
|
+ mobile: string;
|
|
|
+ gender: string;
|
|
|
+ birthDate?: Date;
|
|
|
+ idCard: string;
|
|
|
+ maritalStatus: string;
|
|
|
+ hometown: string;
|
|
|
+ residence: string;
|
|
|
+ occupation: string;
|
|
|
+ others: string;
|
|
|
+};
|
|
|
+
|
|
|
+type FieldKey = "name" | "mobile" | "idCard";
|
|
|
+type FieldErrors = Partial<Record<FieldKey, string>>;
|
|
|
+type BirthDateSource = "unset" | "manual" | "id-card";
|
|
|
+
|
|
|
+type Option = {
|
|
|
+ label: string;
|
|
|
+ value: string;
|
|
|
+};
|
|
|
+
|
|
|
+const GENDER_OPTIONS: Option[] = [
|
|
|
+ { label: "男", value: "male" },
|
|
|
+ { label: "女", value: "female" },
|
|
|
+];
|
|
|
+
|
|
|
+const MARITAL_STATUS_OPTIONS: Option[] = [
|
|
|
+ { label: "未婚", value: "single" },
|
|
|
+ { label: "已婚", value: "married" },
|
|
|
+ { label: "离异", value: "divorced" },
|
|
|
+ { label: "丧偶", value: "widowed" },
|
|
|
+];
|
|
|
+
|
|
|
+const MOBILE_PATTERN = /^1\d{10}$/;
|
|
|
+const IDCARD_PATTERN = /^(?:\d{15}|\d{17}[\dX])$/;
|
|
|
+
|
|
|
+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 getOptionLabel(options: Option[], value: string) {
|
|
|
+ return options.find((item) => item.value === value)?.label ?? "";
|
|
|
+}
|
|
|
+
|
|
|
+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 FieldLabel({
|
|
|
+ label,
|
|
|
+ required = false,
|
|
|
+}: {
|
|
|
+ label: string;
|
|
|
+ required?: boolean;
|
|
|
+}) {
|
|
|
+ return (
|
|
|
+ <View className="mb-2 flex-row items-center">
|
|
|
+ <Text className="text-sm font-semibold text-on-surface">{label}</Text>
|
|
|
+ {required ? <Text className="ml-1 text-sm font-semibold text-red-500">*</Text> : null}
|
|
|
+ </View>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+function FieldMessage({
|
|
|
+ error,
|
|
|
+ helper,
|
|
|
+}: {
|
|
|
+ error?: string;
|
|
|
+ helper?: string;
|
|
|
+}) {
|
|
|
+ if (error) {
|
|
|
+ return <Text className="mt-2 text-xs leading-5 text-red-500">{error}</Text>;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (helper) {
|
|
|
+ return <Text className="mt-2 text-xs leading-5 text-on-surface-variant">{helper}</Text>;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 (
|
|
|
+ <View className="mb-4">
|
|
|
+ <FieldLabel label={label} required={required} />
|
|
|
+ <Pressable
|
|
|
+ disabled={disabled}
|
|
|
+ onPress={onPress}
|
|
|
+ className={`flex-row items-center justify-between rounded-2xl border px-4 py-4 ${error
|
|
|
+ ? "border-red-300 bg-red-50"
|
|
|
+ : "border-outline-variant bg-surface-container-low"
|
|
|
+ }`}
|
|
|
+ style={({ pressed }) => ({
|
|
|
+ opacity: disabled ? 0.55 : pressed ? 0.9 : 1,
|
|
|
+ })}
|
|
|
+ >
|
|
|
+ <Text
|
|
|
+ className={`flex-1 text-base ${hasValue ? "text-on-surface" : "text-outline"}`}
|
|
|
+ numberOfLines={1}
|
|
|
+ >
|
|
|
+ {hasValue ? value : placeholder}
|
|
|
+ </Text>
|
|
|
+ <Ionicons
|
|
|
+ name="chevron-down"
|
|
|
+ size={18}
|
|
|
+ color={error ? "#ef4444" : Colors.color_text_paragraph}
|
|
|
+ />
|
|
|
+ </Pressable>
|
|
|
+ <FieldMessage error={error} helper={helper} />
|
|
|
+ </View>
|
|
|
+ );
|
|
|
+}
|
|
|
|
|
|
export default function AddCustomerScreen() {
|
|
|
- return <View className="flex-1">
|
|
|
+ const insets = useSafeAreaInsets();
|
|
|
+ const navigation = useNavigation();
|
|
|
+ const [form, setForm] = useState<CustomerForm>({
|
|
|
+ name: "",
|
|
|
+ mobile: "",
|
|
|
+ gender: "",
|
|
|
+ birthDate: undefined,
|
|
|
+ idCard: "",
|
|
|
+ maritalStatus: "",
|
|
|
+ hometown: "",
|
|
|
+ residence: "",
|
|
|
+ occupation: "",
|
|
|
+ others: "",
|
|
|
+ });
|
|
|
+ const [errors, setErrors] = useState<FieldErrors>({});
|
|
|
+ const [birthDateSource, setBirthDateSource] = useState<BirthDateSource>("unset");
|
|
|
+ const leaveConfirmVisibleRef = useRef(false);
|
|
|
+
|
|
|
+ const hasDraft = Boolean(
|
|
|
+ form.name.trim() ||
|
|
|
+ form.mobile.trim() ||
|
|
|
+ form.gender ||
|
|
|
+ form.birthDate ||
|
|
|
+ form.idCard.trim() ||
|
|
|
+ form.maritalStatus ||
|
|
|
+ form.hometown.trim() ||
|
|
|
+ form.residence.trim() ||
|
|
|
+ form.occupation.trim() ||
|
|
|
+ form.others.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 }) => {
|
|
|
+ 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.birthDate;
|
|
|
+
|
|
|
+ setForm((prev) => ({
|
|
|
+ ...prev,
|
|
|
+ idCard: nextIdCard,
|
|
|
+ birthDate:
|
|
|
+ inferredBirthDate && canAutofillBirthDate
|
|
|
+ ? inferredBirthDate
|
|
|
+ : !inferredBirthDate && birthDateSource === "id-card"
|
|
|
+ ? undefined
|
|
|
+ : prev.birthDate,
|
|
|
+ }));
|
|
|
+
|
|
|
+ if (inferredBirthDate && canAutofillBirthDate) {
|
|
|
+ setBirthDateSource("id-card");
|
|
|
+ } else if (!inferredBirthDate && birthDateSource === "id-card") {
|
|
|
+ setBirthDateSource("unset");
|
|
|
+ }
|
|
|
+
|
|
|
+ clearErrors(["mobile", "idCard"]);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSubmit = () => {
|
|
|
+ const nextErrors = validateForm(form);
|
|
|
+ setErrors(nextErrors);
|
|
|
+
|
|
|
+ const firstError = Object.values(nextErrors)[0];
|
|
|
+ if (firstError) {
|
|
|
+ Toast.fail(firstError);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Toast.success("表单校验通过,后续可直接接入保存接口");
|
|
|
+ };
|
|
|
+
|
|
|
+
|
|
|
+ const birthDateHelper =
|
|
|
+ birthDateSource === "id-card"
|
|
|
+ ? "已根据身份证自动识别,可继续手动调整"
|
|
|
+ : "填写身份证后可自动识别,也可以手动选择";
|
|
|
+
|
|
|
+ return (
|
|
|
+ <View className="flex-1 bg-surface">
|
|
|
+ <Stack.Screen options={{ title: "添加客户" }} />
|
|
|
<KeyboardAvoidingView
|
|
|
className="flex-1"
|
|
|
- behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
|
+ behavior={Platform.OS === "ios" ? "padding" : undefined}
|
|
|
>
|
|
|
<ScrollView
|
|
|
className="flex-1"
|
|
|
- contentContainerClassName="px-8"
|
|
|
-
|
|
|
+ contentContainerClassName="px-5 pb-10"
|
|
|
+ contentContainerStyle={{
|
|
|
+ paddingTop: (insets.top ?? 0) + 60,
|
|
|
+ paddingBottom: (insets.bottom ?? 0) + 28,
|
|
|
+ }}
|
|
|
keyboardShouldPersistTaps="handled"
|
|
|
showsVerticalScrollIndicator={false}
|
|
|
>
|
|
|
- <View className="absolute -top-24 -right-24 h-96 w-96 rounded-full bg-primary-container/10" />
|
|
|
- <View className="absolute top-32 -left-20 h-64 w-64 rounded-full bg-secondary-container/20" />
|
|
|
- <View className="absolute -bottom-16 right-0 h-48 w-48 rounded-full bg-primary-fixed/50" />
|
|
|
- <Button type="primary">ddd</Button>
|
|
|
- <Button type="primary">ddd</Button>
|
|
|
- <Button type="primary">ddd</Button>
|
|
|
- <Button type="primary">ddd</Button>
|
|
|
- <Button type="primary">ddd</Button>
|
|
|
- <Button type="primary">ddd</Button>
|
|
|
- </ScrollView>
|
|
|
- </KeyboardAvoidingView>
|
|
|
+ <View className="mb-6">
|
|
|
+ <Text className="text-3xl font-extrabold tracking-tight text-on-surface">
|
|
|
+ 新建客户
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <View className="mb-5 rounded-3xl border border-blue-100 bg-primary-fixed px-4 py-4">
|
|
|
+ <View className="flex-row items-start">
|
|
|
+ <View className="mr-3 mt-0.5 h-10 w-10 items-center justify-center rounded-2xl bg-primary-container">
|
|
|
+ <Ionicons name="information-circle-outline" size={20} color="#ffffff" />
|
|
|
+ </View>
|
|
|
+ <View className="flex-1">
|
|
|
+ <Text className="text-base font-bold text-on-surface">提示:</Text>
|
|
|
+ <Text className="mt-1 text-sm leading-6 text-on-surface-variant">
|
|
|
+ 您可以先<Link href="/credit/select" replace asChild><Text className="text-primary font-bold">上传分析征信</Text></Link>,然后从已分析征信中<Link href="/credit/select" asChild><Text className="text-primary font-bold">选择</Text></Link>一条信息以填充客户资料。
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+
|
|
|
+
|
|
|
+ <View className="mb-4">
|
|
|
+ <FieldLabel label="姓名" required />
|
|
|
+ <Input
|
|
|
+ value={form.name}
|
|
|
+ onChangeText={handleNameChange}
|
|
|
+ placeholder="请输入客户姓名"
|
|
|
+ allowClear
|
|
|
+ status={errors.name ? "error" : undefined}
|
|
|
+ style={[
|
|
|
+ styles.inputContainer,
|
|
|
+ errors.name ? styles.inputContainerError : undefined,
|
|
|
+ ]}
|
|
|
+ inputStyle={styles.inputText}
|
|
|
+ />
|
|
|
+ <FieldMessage error={errors.name} />
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <Picker
|
|
|
+ data={GENDER_OPTIONS}
|
|
|
+ cols={1}
|
|
|
+ value={form.gender ? [form.gender] : []}
|
|
|
+ onOk={(values) => {
|
|
|
+ const selected = values[0];
|
|
|
+ setForm((prev) => ({
|
|
|
+ ...prev,
|
|
|
+ gender: typeof selected === "string" ? selected : String(selected ?? ""),
|
|
|
+ }));
|
|
|
+ }}
|
|
|
+ format={(labels) => labels[0] ?? ""}
|
|
|
+ >
|
|
|
+ {({ disabled, toggle }) => (
|
|
|
+ <PickerField
|
|
|
+ label="性别"
|
|
|
+ value={getOptionLabel(GENDER_OPTIONS, form.gender)}
|
|
|
+ placeholder="请选择性别"
|
|
|
+ disabled={disabled}
|
|
|
+ onPress={toggle}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </Picker>
|
|
|
+
|
|
|
+ <DatePicker
|
|
|
+ precision="day"
|
|
|
+ value={form.birthDate}
|
|
|
+ minDate={new Date(1950, 0, 1)}
|
|
|
+ maxDate={new Date()}
|
|
|
+ format={formatDate}
|
|
|
+ onOk={(value) => {
|
|
|
+ setForm((prev) => ({ ...prev, birthDate: value }));
|
|
|
+ setBirthDateSource("manual");
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {({ disabled, toggle }) => (
|
|
|
+ <PickerField
|
|
|
+ label="出生日期"
|
|
|
+ value={formatDate(form.birthDate)}
|
|
|
+ placeholder="请选择出生日期"
|
|
|
+ helper={birthDateHelper}
|
|
|
+ disabled={disabled}
|
|
|
+ onPress={toggle}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </DatePicker>
|
|
|
+
|
|
|
+ <View className="mb-4">
|
|
|
+ <FieldLabel label="手机号" />
|
|
|
+ <Input
|
|
|
+ value={form.mobile}
|
|
|
+ onChangeText={handleMobileChange}
|
|
|
+ placeholder="请输入 11 位手机号"
|
|
|
+ allowClear
|
|
|
+ keyboardType="phone-pad"
|
|
|
+ textContentType="telephoneNumber"
|
|
|
+ maxLength={11}
|
|
|
+ status={errors.mobile ? "error" : undefined}
|
|
|
+ style={[
|
|
|
+ styles.inputContainer,
|
|
|
+ errors.mobile ? styles.inputContainerError : undefined,
|
|
|
+ ]}
|
|
|
+ inputStyle={styles.inputText}
|
|
|
+ />
|
|
|
+ <FieldMessage
|
|
|
+ error={errors.mobile}
|
|
|
+ helper={errors.mobile ? undefined : "手机号和身份证号至少填写一项"}
|
|
|
+ />
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <View className="mb-4">
|
|
|
+ <FieldLabel label="身份证号" />
|
|
|
+ <Input
|
|
|
+ value={form.idCard}
|
|
|
+ onChangeText={handleIdCardChange}
|
|
|
+ placeholder="请输入身份证号"
|
|
|
+ allowClear
|
|
|
+ autoCapitalize="characters"
|
|
|
+ maxLength={18}
|
|
|
+ status={errors.idCard ? "error" : undefined}
|
|
|
+ style={[
|
|
|
+ styles.inputContainer,
|
|
|
+ errors.idCard ? styles.inputContainerError : undefined,
|
|
|
+ ]}
|
|
|
+ inputStyle={styles.inputText}
|
|
|
+ />
|
|
|
+ <FieldMessage
|
|
|
+ error={errors.idCard}
|
|
|
+ helper={errors.idCard ? undefined : "填写 18 位身份证后会自动识别出生日期"}
|
|
|
+ />
|
|
|
+ </View>
|
|
|
+
|
|
|
+
|
|
|
+ <Picker
|
|
|
+ data={MARITAL_STATUS_OPTIONS}
|
|
|
+ cols={1}
|
|
|
+ value={form.maritalStatus ? [form.maritalStatus] : []}
|
|
|
+ onOk={(values) => {
|
|
|
+ const selected = values[0];
|
|
|
+ setForm((prev) => ({
|
|
|
+ ...prev,
|
|
|
+ maritalStatus:
|
|
|
+ typeof selected === "string" ? selected : String(selected ?? ""),
|
|
|
+ }));
|
|
|
+ }}
|
|
|
+ format={(labels) => labels[0] ?? ""}
|
|
|
+ >
|
|
|
+ {({ disabled, toggle }) => (
|
|
|
+ <PickerField
|
|
|
+ label="婚姻状态"
|
|
|
+ value={getOptionLabel(MARITAL_STATUS_OPTIONS, form.maritalStatus)}
|
|
|
+ placeholder="请选择婚姻状态"
|
|
|
+ disabled={disabled}
|
|
|
+ onPress={toggle}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </Picker>
|
|
|
+
|
|
|
+ <View className="mb-4">
|
|
|
+ <FieldLabel label="户籍所在地" />
|
|
|
+ <Input
|
|
|
+ value={form.hometown}
|
|
|
+ onChangeText={(value) => setForm((prev) => ({ ...prev, hometown: value }))}
|
|
|
+ placeholder="请输入户籍所在地"
|
|
|
+ allowClear
|
|
|
+ style={styles.inputContainer}
|
|
|
+ inputStyle={styles.inputText}
|
|
|
+ />
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <View className="mb-4">
|
|
|
+ <FieldLabel label="现居住地" />
|
|
|
+ <Input
|
|
|
+ value={form.residence}
|
|
|
+ onChangeText={(value) => setForm((prev) => ({ ...prev, residence: value }))}
|
|
|
+ placeholder="请输入现居住地"
|
|
|
+ allowClear
|
|
|
+ style={styles.inputContainer}
|
|
|
+ inputStyle={styles.inputText}
|
|
|
+ />
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <View className="mb-4">
|
|
|
+ <FieldLabel label="职业" />
|
|
|
+ <Input
|
|
|
+ value={form.occupation}
|
|
|
+ onChangeText={(value) => setForm((prev) => ({ ...prev, occupation: value }))}
|
|
|
+ placeholder="请输入职业"
|
|
|
+ allowClear
|
|
|
+ style={styles.inputContainer}
|
|
|
+ inputStyle={styles.inputText}
|
|
|
+ />
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <View>
|
|
|
+ <FieldLabel label="其它" />
|
|
|
+ <Input.TextArea
|
|
|
+ value={form.others}
|
|
|
+ onChangeText={(value) => setForm((prev) => ({ ...prev, others: value }))}
|
|
|
+ placeholder="补充备注、渠道来源、客户标签等"
|
|
|
+ autoSize={{ minRows: 4, maxRows: 6 }}
|
|
|
+ maxLength={200}
|
|
|
+ showCount
|
|
|
+ allowClear
|
|
|
+ style={styles.textAreaContainer}
|
|
|
+ inputStyle={styles.textAreaText}
|
|
|
+ />
|
|
|
+ </View>
|
|
|
+
|
|
|
+ <UIButton type="primary" icon="save" onPress={handleSubmit}>
|
|
|
+ 保存客户
|
|
|
+ </UIButton>
|
|
|
+ </ScrollView>
|
|
|
+ </KeyboardAvoidingView>
|
|
|
</View>
|
|
|
-}
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+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,
|
|
|
+ },
|
|
|
+ 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",
|
|
|
+ },
|
|
|
+});
|