add.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743
  1. import { pressableStyle } from "nativewind";
  2. import { DatePicker, Input, Modal, Radio, Toast } from "@ant-design/react-native";
  3. import { Ionicons } from "@expo/vector-icons";
  4. import { usePreventRemove, useRoute } from "@react-navigation/native";
  5. import { Link, Stack, useNavigation } from "expo-router";
  6. import React, { useCallback, useRef, useState } from "react";
  7. import {
  8. KeyboardAvoidingView,
  9. Platform,
  10. Pressable,
  11. ScrollView,
  12. StyleSheet,
  13. Text,
  14. TextInput,
  15. View,
  16. } from "react-native";
  17. import { useSafeAreaInsets } from "react-native-safe-area-context";
  18. import UIButton from "@/components/ui/ui-button";
  19. import { Colors } from "@/constants/theme";
  20. import api from "@/utils/api";
  21. import { UploadScreen } from "../../components/upload";
  22. import type { NavigationAction } from "@react-navigation/routers";
  23. type CustomerForm = {
  24. name: string;
  25. mobile: string;
  26. gender: string;
  27. birthday?: Date;
  28. idcard: string;
  29. nation: string;
  30. registered: string;
  31. residential: string;
  32. photo: string;
  33. occupation: string;
  34. other: string;
  35. };
  36. type FieldKey = "name" | "mobile" | "idcard";
  37. type FieldErrors = Partial<Record<FieldKey, string>>;
  38. type BirthDateSource = "unset" | "manual" | "id-card";
  39. type Option = {
  40. label: string;
  41. value: string;
  42. };
  43. const GENDER_OPTIONS: Option[] = [
  44. { label: "男", value: "1" },
  45. { label: "女", value: "2" },
  46. { label: "其它", value: "3" }
  47. ];
  48. const MOBILE_PATTERN = /^1\d{10}$/;
  49. const IDCARD_PATTERN = /^(?:\d{15}|\d{17}[\dX])$/;
  50. const EMPTY_CUSTOMER_FORM: CustomerForm = {
  51. name: "",
  52. mobile: "",
  53. gender: "",
  54. birthday: undefined,
  55. idcard: "",
  56. nation: "",
  57. registered: "",
  58. residential: "",
  59. photo: "",
  60. occupation: "",
  61. other: "",
  62. };
  63. type CustomerExtPayload = {
  64. nation: string;
  65. registered: string | null;
  66. residential: string | null;
  67. photo: string | null;
  68. occupation: string | null;
  69. other: string | null;
  70. };
  71. type CustomerPayload = {
  72. name: string;
  73. mobile: string;
  74. gender: number;
  75. birthday: string | null;
  76. idcard: string;
  77. ext: CustomerExtPayload;
  78. };
  79. function sanitizePhone(value: string) {
  80. return value.replace(/\D/g, "").slice(0, 11);
  81. }
  82. function sanitizeIdCard(value: string) {
  83. return value.replace(/[^0-9xX]/g, "").toUpperCase().slice(0, 18);
  84. }
  85. function padZero(value: number) {
  86. return String(value).padStart(2, "0");
  87. }
  88. function formatDate(date?: Date) {
  89. if (!date) {
  90. return "";
  91. }
  92. return `${date.getFullYear()}-${padZero(date.getMonth() + 1)}-${padZero(date.getDate())}`;
  93. }
  94. function isValidDatePart(year: number, month: number, day: number) {
  95. if (year < 1900 || year > new Date().getFullYear()) {
  96. return false;
  97. }
  98. const date = new Date(year, month - 1, day);
  99. return (
  100. date.getFullYear() === year &&
  101. date.getMonth() === month - 1 &&
  102. date.getDate() === day &&
  103. date.getTime() <= Date.now()
  104. );
  105. }
  106. function inferBirthDateFromIdCard(idCard: string) {
  107. if (idCard.length < 14) {
  108. return undefined;
  109. }
  110. const birthText = idCard.slice(6, 14);
  111. if (!/^\d{8}$/.test(birthText)) {
  112. return undefined;
  113. }
  114. const year = Number(birthText.slice(0, 4));
  115. const month = Number(birthText.slice(4, 6));
  116. const day = Number(birthText.slice(6, 8));
  117. if (!isValidDatePart(year, month, day)) {
  118. return undefined;
  119. }
  120. return new Date(year, month - 1, day);
  121. }
  122. function nullableText(value: string) {
  123. const trimmed = value.trim();
  124. return trimmed || null;
  125. }
  126. function validateForm(form: CustomerForm): FieldErrors {
  127. const nextErrors: FieldErrors = {};
  128. const mobile = form.mobile.trim();
  129. const idcard = form.idcard.trim();
  130. if (!form.name.trim()) {
  131. nextErrors.name = "请输入客户姓名";
  132. }
  133. if (!mobile && !idcard) {
  134. nextErrors.mobile = "手机号和身份证号至少填写一项";
  135. nextErrors.idcard = "手机号和身份证号至少填写一项";
  136. return nextErrors;
  137. }
  138. if (mobile && !MOBILE_PATTERN.test(mobile)) {
  139. nextErrors.mobile = "请输入 11 位手机号";
  140. }
  141. if (idcard && !IDCARD_PATTERN.test(idcard)) {
  142. nextErrors.idcard = "请输入正确的身份证号";
  143. }
  144. return nextErrors;
  145. }
  146. function buildCustomerPayload(form: CustomerForm): CustomerPayload {
  147. return {
  148. name: form.name.trim(),
  149. mobile: form.mobile.trim(),
  150. gender: Number(form.gender) || 0,
  151. birthday: form.birthday ? formatDate(form.birthday) : null,
  152. idcard: form.idcard.trim(),
  153. ext: {
  154. nation: form.nation.trim(),
  155. registered: nullableText(form.registered),
  156. residential: nullableText(form.residential),
  157. photo: nullableText(form.photo),
  158. occupation: nullableText(form.occupation),
  159. other: nullableText(form.other),
  160. },
  161. };
  162. }
  163. function FieldLabel({
  164. label,
  165. required = false,
  166. }: {
  167. label: string;
  168. required?: boolean;
  169. }) {
  170. return (
  171. <View className="mb-2 flex-row items-center">
  172. <Text className="text-sm font-semibold text-on-surface">{label}</Text>
  173. {required ? <Text className="ml-1 text-sm font-semibold text-red-500">*</Text> : null}
  174. </View>
  175. );
  176. }
  177. function FieldMessage({
  178. error,
  179. helper,
  180. }: {
  181. error?: string;
  182. helper?: string;
  183. }) {
  184. if (error) {
  185. return <Text className="mt-2 text-xs leading-5 text-red-500">{error}</Text>;
  186. }
  187. if (helper) {
  188. return <Text className="mt-2 text-xs leading-5 text-on-surface-variant">{helper}</Text>;
  189. }
  190. return null;
  191. }
  192. function PickerField({
  193. label,
  194. required = false,
  195. value,
  196. placeholder,
  197. error,
  198. helper,
  199. disabled = false,
  200. onPress,
  201. }: {
  202. label: string;
  203. required?: boolean;
  204. value?: string;
  205. placeholder: string;
  206. error?: string;
  207. helper?: string;
  208. disabled?: boolean;
  209. onPress: () => void;
  210. }) {
  211. const hasValue = Boolean(value);
  212. return (
  213. <View className="mb-4">
  214. <FieldLabel label={label} required={required} />
  215. <Pressable
  216. disabled={disabled}
  217. onPress={onPress}
  218. className={`flex-row items-center justify-between rounded-2xl border px-4 py-4 ${error
  219. ? "border-red-300 bg-red-50"
  220. : "border-outline-variant bg-surface-container-low"
  221. }`}
  222. style={pressableStyle(({ pressed }) => ({
  223. opacity: disabled ? 0.55 : pressed ? 0.9 : 1,
  224. }))}
  225. >
  226. <Text
  227. className={`flex-1 text-base ${hasValue ? "text-on-surface" : "text-outline"}`}
  228. numberOfLines={1}
  229. >
  230. {hasValue ? value : placeholder}
  231. </Text>
  232. <Ionicons
  233. name="chevron-down"
  234. size={18}
  235. color={error ? "#ef4444" : Colors.color_text_paragraph}
  236. />
  237. </Pressable>
  238. <FieldMessage error={error} helper={helper} />
  239. </View>
  240. );
  241. }
  242. export default function AddCustomerScreen() {
  243. const insets = useSafeAreaInsets();
  244. const navigation = useNavigation();
  245. const [form, setForm] = useState<CustomerForm>(EMPTY_CUSTOMER_FORM);
  246. const [errors, setErrors] = useState<FieldErrors>({});
  247. const [birthDateSource, setBirthDateSource] = useState<BirthDateSource>("unset");
  248. const [saving, setSaving] = useState(false);
  249. const leaveConfirmVisibleRef = useRef(false);
  250. const savedLeaveRef = useRef(false);
  251. const route = useRoute();
  252. const scrollViewRef = useRef<ScrollView>(null!);
  253. const hasDraft = Boolean(
  254. form.name.trim() ||
  255. form.mobile.trim() ||
  256. form.gender ||
  257. form.birthday ||
  258. form.idcard.trim() ||
  259. form.nation.trim() ||
  260. form.registered.trim() ||
  261. form.residential.trim() ||
  262. form.photo.trim() ||
  263. form.occupation.trim() ||
  264. form.other.trim()
  265. );
  266. const openLeaveConfirm = useCallback(
  267. (action: NavigationAction) => {
  268. if (leaveConfirmVisibleRef.current) {
  269. return;
  270. }
  271. leaveConfirmVisibleRef.current = true;
  272. Modal.alert(
  273. "放弃当前编辑",
  274. "当前页面还有未保存的客户资料,确定要返回吗?",
  275. [
  276. {
  277. text: "继续编辑",
  278. style: "cancel",
  279. onPress: () => {
  280. leaveConfirmVisibleRef.current = false;
  281. },
  282. },
  283. {
  284. text: "确认返回",
  285. style: "destructive",
  286. onPress: () => {
  287. leaveConfirmVisibleRef.current = false;
  288. navigation.dispatch(action);
  289. },
  290. },
  291. ],
  292. () => {
  293. leaveConfirmVisibleRef.current = false;
  294. return true;
  295. }
  296. );
  297. },
  298. [navigation]
  299. );
  300. usePreventRemove(hasDraft, ({ data }) => {
  301. if (savedLeaveRef.current) {
  302. navigation.dispatch(data.action);
  303. return;
  304. }
  305. openLeaveConfirm(data.action);
  306. });
  307. const clearErrors = (keys: FieldKey[]) => {
  308. setErrors((prev) => {
  309. const next = { ...prev };
  310. keys.forEach((key) => {
  311. delete next[key];
  312. });
  313. return next;
  314. });
  315. };
  316. const handleNameChange = (value: string) => {
  317. setForm((prev) => ({ ...prev, name: value }));
  318. clearErrors(["name"]);
  319. };
  320. const handleMobileChange = (value: string) => {
  321. setForm((prev) => ({ ...prev, mobile: sanitizePhone(value) }));
  322. clearErrors(["mobile", "idcard"]);
  323. };
  324. const handleIdCardChange = (value: string) => {
  325. const nextIdCard = sanitizeIdCard(value);
  326. const inferredBirthDate = inferBirthDateFromIdCard(nextIdCard);
  327. const canAutofillBirthDate = birthDateSource !== "manual" || !form.birthday;
  328. setForm((prev) => ({
  329. ...prev,
  330. idcard: nextIdCard,
  331. birthday:
  332. inferredBirthDate && canAutofillBirthDate
  333. ? inferredBirthDate
  334. : !inferredBirthDate && birthDateSource === "id-card"
  335. ? undefined
  336. : prev.birthday,
  337. }));
  338. if (inferredBirthDate && canAutofillBirthDate) {
  339. setBirthDateSource("id-card");
  340. } else if (!inferredBirthDate && birthDateSource === "id-card") {
  341. setBirthDateSource("unset");
  342. }
  343. clearErrors(["mobile", "idcard"]);
  344. };
  345. const savedRef = useRef<boolean>(false);
  346. const onSelectCredit = (isCancel: boolean) => {
  347. setSelectCredit(false);
  348. // if (isCancel) {
  349. // return;
  350. // }
  351. if (savedRef.current || !isCancel) {
  352. navigation.goBack();
  353. return;
  354. }
  355. }
  356. const handleSubmit = async () => {
  357. if (saving) {
  358. return;
  359. }
  360. const nextErrors = validateForm(form);
  361. setErrors(nextErrors);
  362. const firstError = Object.values(nextErrors)[0];
  363. if (firstError) {
  364. Toast.fail(firstError);
  365. scrollViewRef.current?.scrollTo({ y: 160, animated: true });
  366. return;
  367. }
  368. setSaving(true);
  369. const toastKey = Toast.loading("正在保存客户...");
  370. try {
  371. const customerId = await api.post("/customer/add", buildCustomerPayload(form));
  372. Toast.success("客户已保存");
  373. savedLeaveRef.current = true;
  374. // setForm(EMPTY_CUSTOMER_FORM);
  375. // setBirthDateSource("unset");
  376. // setErrors({});
  377. // @ts-ignore
  378. if (route.params?.onGoBack) {
  379. // @ts-ignore
  380. route.params.onGoBack({ customerId });
  381. navigation.goBack();
  382. } else {
  383. Modal.alert("客户添加完成", "是否立即上传征信信息", [{
  384. text: '否',
  385. }, {
  386. 'text': '立即上传',
  387. onPress: () => {
  388. setSelectCredit(true);
  389. }
  390. }])
  391. }
  392. } catch (error) {
  393. console.error("保存客户失败:", error);
  394. Toast.fail(error instanceof Error ? error.message : "保存客户失败,请稍后重试");
  395. } finally {
  396. setSaving(false);
  397. Toast.remove(toastKey);
  398. }
  399. };
  400. const birthDateHelper =
  401. birthDateSource === "id-card"
  402. ? "已根据身份证自动识别,可继续手动调整"
  403. : "填写身份证后可自动识别,也可以手动选择";
  404. const [selectCredit, setSelectCredit] = useState(false);
  405. return (
  406. <View className="flex-1 bg-surface">
  407. <Stack.Screen options={{ title: "添加客户" }} />
  408. <KeyboardAvoidingView
  409. className="flex-1"
  410. behavior={Platform.OS === "ios" ? "padding" : undefined}
  411. >
  412. <ScrollView
  413. ref={scrollViewRef}
  414. className="flex-1"
  415. contentContainerClassName="px-5 pb-10"
  416. contentContainerStyle={{
  417. paddingTop: (insets.top ?? 0) + 60,
  418. paddingBottom: (insets.bottom ?? 0) + 28,
  419. }}
  420. keyboardShouldPersistTaps="handled"
  421. showsVerticalScrollIndicator={false}
  422. >
  423. <View className="mb-6">
  424. <Text className="text-3xl font-extrabold tracking-tight text-on-surface">
  425. 新建客户
  426. </Text>
  427. </View>
  428. <View className="mb-5 rounded-3xl border border-blue-100 bg-primary-fixed px-4 py-4">
  429. <View className="flex-row items-start">
  430. <View className="mr-3 mt-0.5 h-10 w-10 items-center justify-center rounded-2xl bg-primary-container">
  431. <Ionicons name="information-circle-outline" size={20} color="#ffffff" />
  432. </View>
  433. <View className="flex-1">
  434. <Text className="text-base font-bold text-on-surface">提示:</Text>
  435. <Text className="mt-1 text-sm leading-6 text-on-surface-variant">
  436. 您可以先<Text className="text-primary font-bold" onPress={() => setSelectCredit(true)}>上传分析征信</Text>,然后从已分析征信中<Link href="/credit/select" asChild><Text className="text-primary font-bold">选择</Text></Link>一条信息以填充客户资料。
  437. </Text>
  438. </View>
  439. </View>
  440. </View>
  441. <View className="mb-4">
  442. <FieldLabel label="姓名" required />
  443. <Input
  444. value={form.name}
  445. onChangeText={handleNameChange}
  446. placeholder="请输入客户姓名"
  447. allowClear
  448. status={errors.name ? "error" : undefined}
  449. style={[
  450. styles.inputContainer,
  451. errors.name ? styles.inputContainerError : undefined,
  452. ]}
  453. inputStyle={styles.inputText}
  454. />
  455. <FieldMessage error={errors.name} />
  456. </View>
  457. <View className="mb-4">
  458. <FieldLabel label="性别" />
  459. <Radio.Group
  460. value={form.gender}
  461. onChange={(event) => {
  462. const selected = event.target.value;
  463. if (typeof selected !== "string" && typeof selected !== "number") {
  464. return;
  465. }
  466. setForm((prev) => ({
  467. ...prev,
  468. gender: String(selected),
  469. }));
  470. }}
  471. style={styles.radioGroup}
  472. >
  473. {GENDER_OPTIONS.map((option) => (
  474. <Radio key={option.value} value={option.value}>
  475. {option.label}
  476. </Radio>
  477. ))}
  478. </Radio.Group>
  479. </View>
  480. <DatePicker
  481. precision="day"
  482. value={form.birthday}
  483. minDate={new Date(1950, 0, 1)}
  484. maxDate={new Date()}
  485. format={formatDate}
  486. onOk={(value) => {
  487. setForm((prev) => ({ ...prev, birthday: value }));
  488. setBirthDateSource("manual");
  489. }}
  490. >
  491. {({ disabled, toggle }) => (
  492. <PickerField
  493. label="出生日期"
  494. value={formatDate(form.birthday)}
  495. placeholder="请选择出生日期"
  496. helper={birthDateHelper}
  497. disabled={disabled}
  498. onPress={toggle}
  499. />
  500. )}
  501. </DatePicker>
  502. <View className="mb-4">
  503. <FieldLabel label="手机号" />
  504. <TextInput
  505. value={form.mobile}
  506. onChangeText={handleMobileChange}
  507. placeholder="请输入 11 位手机号"
  508. keyboardType="phone-pad"
  509. placeholderTextColor="#9ca3af"
  510. textContentType="telephoneNumber"
  511. underlineColorAndroid="transparent"
  512. maxLength={11}
  513. style={[
  514. styles.inputContainer,
  515. styles.inputText,
  516. errors.mobile ? styles.inputContainerError : undefined,
  517. ]}
  518. />
  519. <FieldMessage
  520. error={errors.mobile}
  521. helper={errors.mobile ? undefined : "手机号和身份证号至少填写一项"}
  522. />
  523. </View>
  524. <View className="mb-4">
  525. <FieldLabel label="身份证号" />
  526. <TextInput
  527. value={form.idcard}
  528. onChangeText={handleIdCardChange}
  529. placeholder="请输入身份证号"
  530. autoCapitalize="characters"
  531. autoCorrect={false}
  532. placeholderTextColor="#9ca3af"
  533. underlineColorAndroid="transparent"
  534. maxLength={18}
  535. style={[
  536. styles.inputContainer,
  537. styles.inputText,
  538. errors.idcard ? styles.inputContainerError : undefined,
  539. ]}
  540. />
  541. <FieldMessage
  542. error={errors.idcard}
  543. helper={errors.idcard ? undefined : "填写 18 位身份证后会自动识别出生日期"}
  544. />
  545. </View>
  546. <View className="mb-4">
  547. <FieldLabel label="民族" />
  548. <Input
  549. value={form.nation}
  550. onChangeText={(value) => setForm((prev) => ({ ...prev, nation: value }))}
  551. placeholder="请输入民族"
  552. allowClear
  553. maxLength={16}
  554. style={styles.inputContainer}
  555. inputStyle={styles.inputText}
  556. />
  557. </View>
  558. <View className="mb-4">
  559. <FieldLabel label="户籍所在地" />
  560. <Input
  561. value={form.registered}
  562. onChangeText={(value) => setForm((prev) => ({ ...prev, registered: value }))}
  563. placeholder="请输入户籍所在地"
  564. allowClear
  565. style={styles.inputContainer}
  566. inputStyle={styles.inputText}
  567. />
  568. </View>
  569. <View className="mb-4">
  570. <FieldLabel label="现居住地" />
  571. <Input
  572. value={form.residential}
  573. onChangeText={(value) => setForm((prev) => ({ ...prev, residential: value }))}
  574. placeholder="请输入现居住地"
  575. allowClear
  576. style={styles.inputContainer}
  577. inputStyle={styles.inputText}
  578. />
  579. </View>
  580. <View className="mb-4">
  581. <FieldLabel label="照片" />
  582. <Input
  583. value={form.photo}
  584. onChangeText={(value) => setForm((prev) => ({ ...prev, photo: value }))}
  585. placeholder="请输入照片地址"
  586. allowClear
  587. style={styles.inputContainer}
  588. inputStyle={styles.inputText}
  589. />
  590. </View>
  591. <View className="mb-4">
  592. <FieldLabel label="职业" />
  593. <Input
  594. value={form.occupation}
  595. onChangeText={(value) => setForm((prev) => ({ ...prev, occupation: value }))}
  596. placeholder="请输入职业"
  597. allowClear
  598. style={styles.inputContainer}
  599. inputStyle={styles.inputText}
  600. />
  601. </View>
  602. <View>
  603. <FieldLabel label="其它" />
  604. <Input.TextArea
  605. value={form.other}
  606. onChangeText={(value) => setForm((prev) => ({ ...prev, other: value }))}
  607. placeholder="补充备注、渠道来源、客户标签等"
  608. autoSize={{ minRows: 4, maxRows: 6 }}
  609. maxLength={200}
  610. showCount
  611. allowClear
  612. style={styles.textAreaContainer}
  613. inputStyle={styles.textAreaText}
  614. />
  615. </View>
  616. <UIButton type="primary" icon="save" onPress={handleSubmit} disabled={saving} loading={saving}>
  617. 保存客户
  618. </UIButton>
  619. </ScrollView>
  620. </KeyboardAvoidingView>
  621. <UploadScreen visible={selectCredit} onClose={onSelectCredit} />
  622. </View>
  623. );
  624. }
  625. const styles = StyleSheet.create({
  626. inputContainer: {
  627. minHeight: 54,
  628. borderRadius: 16,
  629. borderWidth: 1,
  630. borderColor: Colors.border_color_base,
  631. backgroundColor: Colors.fill_body,
  632. paddingHorizontal: 14,
  633. },
  634. inputContainerError: {
  635. borderColor: "#fca5a5",
  636. backgroundColor: "#fef2f2",
  637. },
  638. inputText: {
  639. color: Colors.color_text_base,
  640. fontSize: 16,
  641. paddingVertical: Platform.OS === "web" ? 12 : 10,
  642. },
  643. radioGroup: {
  644. flexDirection: "row",
  645. gap: 24,
  646. minHeight: 44,
  647. alignItems: "center",
  648. },
  649. textAreaContainer: {
  650. borderRadius: 16,
  651. borderWidth: 1,
  652. borderColor: Colors.border_color_base,
  653. backgroundColor: Colors.fill_body,
  654. paddingHorizontal: 14,
  655. paddingTop: 12,
  656. paddingBottom: 12,
  657. },
  658. textAreaText: {
  659. color: Colors.color_text_base,
  660. fontSize: 16,
  661. lineHeight: 22,
  662. minHeight: 96,
  663. textAlignVertical: "top",
  664. },
  665. });