improve.vue 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929
  1. <template>
  2. <cwg-page-wrapper>
  3. <view class="page page-shadow">
  4. <u-form ref="formRef" :rules="rules" :model="formData" class="payment-form">
  5. <!-- 第一步:基本信息 -->
  6. <view v-show="currentStep === 1" class="form-section">
  7. <h3 class="section-title">{{ t("card.Info.s1") }}</h3>
  8. <cwg-input v-model:value="formData.lastName" fkey="lastName" :required="true" :label="t('card.Form.f4')"
  9. rulesKey="lastName" :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
  10. <cwg-input v-model:value="formData.firstName" fkey="firstName" :required="true" :label="t('card.Form.f5')"
  11. rulesKey="firstName" :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
  12. <cwg-input v-model:value="formData.email" fkey="email" :label="t('card.Form.f3')" :required="true"
  13. rulesKey="email" :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
  14. <view class="f" v-if="phoneCodes.length > 0">
  15. <cwg-input v-model:value="formData.areaCode" class="l" fkey="areaCode" :required="true" type="select"
  16. :show-search="true" :columns="phoneCodes" :label="t('card.Form.f2')" rulesKey="areaCode"
  17. :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
  18. <cwg-input v-model:value="formData.mobile" class="r" fkey="mobile" label=" " rulesKey="mobile"
  19. :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
  20. </view>
  21. <cwg-input v-model:value="formData.gender" fkey="gender" type="select" :required="true" rulesKey="gender"
  22. :columns="sexOptions" :label="t('card.Form.f8')" :readonly="isReadonly" :disabled="isReadonly"
  23. @change="handleChange" />
  24. <cwg-input v-model:value="formData.birthday" :required="true" type="date" fkey="birthday" rulesKey="birthday"
  25. :label="t('card.Form.f6')" :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
  26. </view>
  27. <!-- 第二步:国籍信息 -->
  28. <view v-show="currentStep === 2" class="form-section">
  29. <h3 class="section-title">{{ t("ImproveImmediately.Title.AddressInformation") }}</h3>
  30. <cwg-input v-if="countryOptions.length > 0" v-model:value="formData.nationality" fkey="nationality"
  31. type="select" :required="true" :show-search="true" rulesKey="nationality" :columns="countryOptions"
  32. :label="t('card.Form.f7')" :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
  33. <cwg-input v-if="cityOptions.length > 0" v-model:value="formData.town" fkey="town" :required="true"
  34. :show-search="true" rulesKey="town" type="select" :columns="cityOptions" :label="t('card.Form.f9')"
  35. :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
  36. <cwg-input v-model:value="formData.address" fkey="address" :label="t('card.Form.f10')" :required="true"
  37. rulesKey="address" :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
  38. <cwg-input v-model:value="formData.postCode" :required="true" fkey="postCode" rulesKey="postCode"
  39. :label="t('card.Form.f11')" :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
  40. </view>
  41. <!-- 第三步:职业信息 -->
  42. <view v-show="currentStep === 3" class="form-section">
  43. <h3 class="section-title">{{ t("card.Info.s0") }}</h3>
  44. <cwg-input v-if="occupationList.length > 0" v-model:value="formData.occupation" fkey="occupation"
  45. type="select" :columns="occupationList" :label="t('card.Form.f12')" :readonly="isReadonly"
  46. :disabled="isReadonly" @change="handleChange" />
  47. <cwg-input v-model:value="formData.annualSalary" fkey="annualSalary" :label="t('card.Form.f13')"
  48. :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
  49. <cwg-input v-model:value="formData.accountPurpose" fkey="accountPurpose" :label="t('card.Form.f14')"
  50. :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
  51. <cwg-input v-model:value="formData.expectedMonthlyVolume" fkey="expectedMonthlyVolume"
  52. :label="t('card.Form.f15')" :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
  53. </view>
  54. <!-- 第四步:证件信息 -->
  55. <view v-show="currentStep === 4" class="form-section">
  56. <h3 class="section-title">{{ t("card.Info.s2") }}</h3>
  57. <cwg-input v-if="availableIdTypeOptions.length" v-model:value="formData.idType" rulesKey="idType"
  58. fkey="idType" :required="true" type="select" :columns="availableIdTypeOptions" :label="t('card.Form.f16')"
  59. :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
  60. <cwg-input v-model:value="formData.idNumber" :required="true" rulesKey="idNumber" fkey="idNumber"
  61. :label="t('card.Form.f17')" :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
  62. <cwg-input v-if="formData.nationality == 'US'" v-model:value="formData.ssn" fkey="ssn" rulesKey="ssn"
  63. :label="t('card.Form.f18')" :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
  64. <cwg-input v-model:value="formData.issueDate" :required="true" type="date" fkey="issueDate"
  65. rulesKey="issueDate" :label="t('card.Form.f19')" :readonly="isReadonly" :disabled="isReadonly"
  66. @change="handleChange" />
  67. <cwg-input v-model:value="formData.idNoExpiryDate" :required="true" type="date" fkey="idNoExpiryDate"
  68. rulesKey="idNoExpiryDate" :label="t('card.Form.f20')" :readonly="isReadonly" :disabled="isReadonly"
  69. @change="handleChange" />
  70. <template v-if="formData.photoStatus == '1'">
  71. <cwg-input v-model:value="formData.idFrontUrl" :required="true" type="upload" fkey="idFrontUrl"
  72. rulesKey="idFrontUrl" :label="t('card.Form.f21')" :is-upload-d="true"
  73. accept="image/png, image/jpeg, image/jpg" :readonly="isReadonly" :disabled="isReadonly"
  74. @change="handleChange">
  75. <view class="cwg-upload">
  76. <cwg-icon name="back-top" />
  77. <view class="name">
  78. {{ t("card.Form.f23_2") }}{{ t("card.Form.f23") }}
  79. </view>
  80. <view class="back">{{ t("card.Form.f23_3") }}</view>
  81. </view>
  82. </cwg-input>
  83. <cwg-input v-model:value="formData.idBackUrl" type="upload" :required="true" fkey="idBackUrl"
  84. rulesKey="idBackUrl" :label="t('card.Form.f22')" :is-upload-d="true"
  85. accept="image/png, image/jpeg, image/jpg" :readonly="isReadonly" :disabled="isReadonly"
  86. @change="handleChange">
  87. <view class="cwg-upload">
  88. <cwg-icon name="back-top" />
  89. <view class="name">
  90. {{ t("card.Form.f23_2") }}{{ t("card.Form.f23") }}
  91. </view>
  92. <view class="back">{{ t("card.Form.f23_3") }}</view>
  93. </view>
  94. </cwg-input>
  95. <cwg-input v-model:value="formData.idHoldUrl" type="upload" fkey="idHoldUrl" :required="true"
  96. rulesKey="idHoldUrl" :label="t('card.Form.f23')" :is-upload-d="true"
  97. accept="image/png, image/jpeg, image/jpg" :readonly="isReadonly" :disabled="isReadonly"
  98. @change="handleChange">
  99. <view class="cwg-upload">
  100. <cwg-icon name="back-top" />
  101. <view class="name">
  102. {{ t("card.Form.f23_2") }}{{ t("card.Form.f23") }}
  103. </view>
  104. <view class="back">{{ t("card.Form.f23_3") }}</view>
  105. </view>
  106. </cwg-input>
  107. </template>
  108. </view>
  109. <!-- 步骤控制按钮 -->
  110. <view class="form-actions">
  111. <template v-if="currentStep === 3">
  112. <view class="fixed-btn">
  113. <view class="cwg-button two-btn">
  114. <u-button plain block class="prev-btn" @click="goStep(2)">
  115. {{ t("card.Btn.Previous") }}
  116. </u-button>
  117. <u-button type="primary" block :loading="loadingStates.validateStep3" @click="validateStep3">
  118. {{ t("card.Btn.Next") }}
  119. </u-button>
  120. </view>
  121. </view>
  122. </template>
  123. <template v-else-if="currentStep === 2">
  124. <view class="fixed-btn">
  125. <view class="cwg-button two-btn">
  126. <u-button plain block class="prev-btn" @click="goStep(1)">
  127. {{ t("card.Btn.Previous") }}
  128. </u-button>
  129. <u-button type="warning" block :loading="loadingStates.validateStep2" @click="validateStep2">
  130. {{ t("card.Btn.Next") }}
  131. </u-button>
  132. </view>
  133. </view>
  134. </template>
  135. <template v-else-if="currentStep === 1">
  136. <view class="fixed-btn">
  137. <view class="cwg-button two-btn">
  138. <u-button type="warning" block :loading="loadingStates.validateStep1" @click="validateStep1">
  139. {{ t("card.Btn.Next") }}
  140. </u-button>
  141. </view>
  142. </view>
  143. </template>
  144. <template v-else-if="currentStep === 4">
  145. <view class="fixed-btn">
  146. <view class="cwg-button two-btn">
  147. <u-button plain block class="prev-btn" @click="goStep(3)">
  148. {{ t("card.Btn.Previous") }}
  149. </u-button>
  150. <u-button v-if="!isUpdate" type="warning" block :loading="loadingStates.validateStep4"
  151. @click="validateStep4(1)">
  152. {{ t("card.Btn.Submit") }}
  153. </u-button>
  154. <u-button v-if="isUpdate && !isAuthInfo" type="warning" block :loading="loadingStates.validateStep4"
  155. @click="validateStep4(2)">
  156. {{ t("card.Btn.Update") }}
  157. </u-button>
  158. <u-button v-if="
  159. isUpdate &&
  160. formData.kycStatus != '2' &&
  161. formData.photoStatus != '1'
  162. " type="warning" block :loading="loadingStates.validateStep4" @click="validateStep4(3)">
  163. {{ t("card.Btn.Auth") }}
  164. </u-button>
  165. </view>
  166. </view>
  167. </template>
  168. </view>
  169. </u-form>
  170. </view>
  171. <view class="form-tab"></view>
  172. </cwg-page-wrapper>
  173. <card-websdk-link ref="cardWebsdkLinkRef" />
  174. </template>
  175. <script setup lang="ts">
  176. import { ref, onMounted, watch, computed } from "vue";
  177. import { pinyin } from "pinyin-pro";
  178. import { useI18n } from "vue-i18n";
  179. import { onLoad } from '@dcloudio/uni-app'
  180. import { userToken } from "@/composables/config";
  181. import { ucardApi } from "@/api/ucard";
  182. import { userApi } from "@/api/user";
  183. import useUserStore from "@/stores/use-user-store";
  184. import useIdTypeOptions from "@/composables/useIdTypeOptions";
  185. import { Patterns, Validators } from "@/utils/validators";
  186. import CardWebsdkLink from "@/components/card-websdkLink.vue";
  187. import useRouter from "@/hooks/useRouter";
  188. const router = useRouter();
  189. const userStore = useUserStore();
  190. const userInfo = computed(() => userStore.userInfo);
  191. const { availableIdTypeOptions, loadIdTypesConfig } = useIdTypeOptions()
  192. console.log(availableIdTypeOptions, 1212121, 'storeIdTypeOptionsstoreIdTypeOptions');
  193. const { t } = useI18n();
  194. const currentStep = ref<number>(1);
  195. const cardWebsdkLinkRef = ref<InstanceType<typeof CardWebsdkLink> | null>(null);
  196. onLoad((options) => {
  197. currentStep.value = parseInt(options?.currentStep || '1', 10);
  198. });
  199. const formRef = ref();
  200. function goStep(step: number) {
  201. currentStep.value = step;
  202. }
  203. const sexOptions = ref([
  204. { text: t("card.Form.v1"), value: "M" },
  205. { text: t("card.Form.v2"), value: "F" },
  206. ]);
  207. function validateName(a: any, b?: any, c?: any) {
  208. const reg = /^[A-Z\s]+$/i;
  209. // Element 风格: (rule, value, callback)
  210. if (typeof c === "function") {
  211. const value = b;
  212. const callback = c;
  213. const val = String(value ?? "").trim();
  214. if (!val) return callback(new Error(t("card.vaildate.v4")));
  215. if (!reg.test(val)) return callback(new Error(t("card.vaildate.v38")));
  216. if (val.length < 2 || val.length > 23)
  217. return callback(new Error(t("card.vaildate.v39")));
  218. const firstName = String(formData.value?.firstName ?? "").trim();
  219. const lastName = String(formData.value?.lastName ?? "").trim();
  220. if (`${firstName} ${lastName}`.length > 23)
  221. return callback(new Error(t("card.vaildate.v40")));
  222. return callback();
  223. }
  224. // Vant 风格: (value) => boolean | string | Promise
  225. const val = String(a ?? "").trim();
  226. if (!val) return t("card.vaildate.v4");
  227. if (!reg.test(val)) return t("card.vaildate.v38");
  228. if (val.length < 2 || val.length > 23) return t("card.vaildate.v39");
  229. const firstName = String(formData.value?.firstName ?? "").trim();
  230. const lastName = String(formData.value?.lastName ?? "").trim();
  231. if (`${firstName} ${lastName}`.length > 23) return t("card.vaildate.v40");
  232. return true;
  233. }
  234. // 生日校验:18 岁以上
  235. function validateBirthday(a: any, b?: any, c?: any) {
  236. // Element 风格: (rule, value, callback)
  237. if (typeof c === "function") {
  238. const value = b;
  239. const callback = c;
  240. const val = value;
  241. if (!val) return callback(new Error(t("card.vaildate.v5")));
  242. const today = new Date();
  243. const birthDate = new Date(val);
  244. let age = today.getFullYear() - birthDate.getFullYear();
  245. const month = today.getMonth() - birthDate.getMonth();
  246. if (month < 0 || (month === 0 && today.getDate() < birthDate.getDate()))
  247. age--;
  248. if (age < 18) return callback(new Error(t("card.New.n3")));
  249. return callback();
  250. }
  251. // uview-plus / Vant 风格: (value) => boolean | string
  252. const val = a;
  253. if (!val) return t("card.vaildate.v5");
  254. const today = new Date();
  255. const birthDate = new Date(val);
  256. let age = today.getFullYear() - birthDate.getFullYear();
  257. const month = today.getMonth() - birthDate.getMonth();
  258. if (month < 0 || (month === 0 && today.getDate() < birthDate.getDate()))
  259. age--;
  260. return age < 18 ? t("card.New.n3") : true;
  261. }
  262. function validateAddress(a: any, b?: any, c?: any) {
  263. // 地址只需要校验长度和基础字符合法性,允许数字、字母、空格等
  264. // Element 风格: (rule, value, callback)
  265. if (typeof c === "function") {
  266. const value = b;
  267. const callback = c;
  268. const val = String(value ?? "").trim();
  269. if (!val) return callback(new Error(t("card.vaildate.v27")));
  270. if (val.length < 2 || val.length > 40)
  271. return callback(new Error(t("card.New.n1")));
  272. if (!Patterns.address.test(val))
  273. return callback(new Error(t("card.New.n1")));
  274. return callback();
  275. }
  276. // Vant 风格: (value) => boolean | string | Promise
  277. const val = String(a ?? "").trim();
  278. if (!val) return t("card.vaildate.v27");
  279. if (val.length < 2 || val.length > 40) return t("card.New.n1");
  280. return Patterns.address.test(val) ? true : t("card.New.n1");
  281. }
  282. // 表单验证规则
  283. const rules = {
  284. areaCode: [Validators.required(t("card.vaildate.v1"))],
  285. mobile: [
  286. Validators.required(t("card.vaildate.v2")),
  287. Validators.pattern(Patterns.mobile, t("card.vaildate.v44")),
  288. ],
  289. email: [
  290. Validators.required(t("card.vaildate.v28")),
  291. Validators.pattern(Patterns.email, t("card.vaildate.v28")),
  292. ],
  293. firstName: [
  294. Validators.required(t("card.vaildate.v3")),
  295. Validators.custom(validateName),
  296. ],
  297. lastName: [
  298. Validators.required(t("card.vaildate.v4")),
  299. Validators.custom(validateName),
  300. ],
  301. birthday: [
  302. Validators.required(t("card.vaildate.v5"), "change"),
  303. Validators.custom(validateBirthday, "change"),
  304. ],
  305. nationality: [Validators.required(t("card.vaildate.v6"), "change")],
  306. town: [Validators.required(t("card.vaildate.v7"), "change")],
  307. address: [
  308. Validators.required(t("card.vaildate.v27")),
  309. Validators.custom(validateAddress),
  310. ],
  311. gender: [Validators.required(t("card.vaildate.v9"), "change")],
  312. occupation: [Validators.required(t("card.vaildate.v10"), "change")],
  313. annualSalary: [Validators.required(t("card.vaildate.v11"))],
  314. accountPurpose: [Validators.required(t("card.vaildate.v12"))],
  315. expectedMonthlyVolume: [Validators.required(t("card.vaildate.v13"))],
  316. idType: [Validators.required(t("card.vaildate.v14"), "change")],
  317. idNumber: [
  318. Validators.required(t("card.vaildate.v15")),
  319. Validators.pattern(Patterns.idNumber, t("card.vaildate.v45")),
  320. ],
  321. ssn: [Validators.required(t("card.vaildate.v16"))],
  322. issueDate: [Validators.required(t("card.vaildate.v17"), "change")],
  323. idNoExpiryDate: [Validators.required(t("card.vaildate.v18"), "change")],
  324. idFrontUrl: [Validators.required(t("card.vaildate.v19"), "change")],
  325. idBackUrl: [Validators.required(t("card.vaildate.v20"), "change")],
  326. idHoldUrl: [Validators.required(t("card.vaildate.v21"), "change")],
  327. postCode: [
  328. Validators.required(t("card.vaildate.v8")),
  329. Validators.pattern(Patterns.postcode, t("card.New.n2")),
  330. ],
  331. };
  332. const formData = ref({
  333. merchantOrderNo: undefined,
  334. cardTypeId: undefined,
  335. areaCode: undefined,
  336. mobile: undefined,
  337. email: undefined,
  338. firstName: undefined,
  339. lastName: undefined,
  340. birthday: undefined,
  341. nationality: undefined,
  342. country: undefined,
  343. town: undefined,
  344. address: undefined,
  345. postCode: undefined,
  346. gender: undefined,
  347. occupation: undefined,
  348. annualSalary: undefined,
  349. accountPurpose: undefined,
  350. expectedMonthlyVolume: undefined,
  351. idType: undefined,
  352. idNumber: undefined,
  353. ssn: undefined,
  354. issueDate: undefined,
  355. idNoExpiryDate: undefined,
  356. idFrontUrl: undefined,
  357. idBackUrl: undefined,
  358. idHoldUrl: undefined,
  359. ipAddress: undefined,
  360. cId: undefined,
  361. customId: undefined,
  362. photoStatus: '1',
  363. kycStatus: '1'
  364. });
  365. const isUpdate = ref(false);
  366. const isAuthInfo = ref(false);
  367. // 国家选项
  368. const countryOptions = ref<Array<{ text: string; value: string }>>([]);
  369. const cityOptions = ref<Array<{ text: string; value: string }>>([]);
  370. const phoneCodes = ref<Array<{ text: string; value: string }>>([]);
  371. const occupationList = ref<Array<{ text: string; value: string }>>([]);
  372. // 获取国家列表
  373. async function getCountryListForSelect() {
  374. try {
  375. const res = await ucardApi.ucardCountryCity({});
  376. if (res.code === 200 || res.code === 0) {
  377. countryOptions.value = res.data.map((item: any) => ({
  378. text: item.enName,
  379. value: item.code,
  380. }));
  381. phoneCodes.value = res.data.map((item: any) => ({
  382. text: `${item.areaCode} ${item.enName}`,
  383. value: item.areaCode,
  384. }));
  385. }
  386. } catch (error) {
  387. console.error('Error loading country list:', error);
  388. countryOptions.value = [];
  389. }
  390. }
  391. // 获取城市列表
  392. async function getCityListForSelect(countryCode: string) {
  393. try {
  394. const res = await ucardApi.ucardCountryCity({ code: countryCode });
  395. if (res.code === 200 || res.code === 0) {
  396. const cityList = res.data.map((item: any) => ({
  397. text: item.enName,
  398. value: item.code,
  399. }));
  400. cityOptions.value = cityList;
  401. }
  402. } catch (error) {
  403. console.error('Error loading city list:', error);
  404. cityOptions.value = [];
  405. }
  406. }
  407. // 获取职业
  408. async function getOccupationList() {
  409. try {
  410. const res = await ucardApi.getOccupationList();
  411. if (res.code === 200 || res.code === 0) {
  412. const list = res.data.map((item: any) => ({
  413. text: item.description,
  414. value: item.occupationCode,
  415. }));
  416. occupationList.value = list;
  417. }
  418. } catch (error) {
  419. console.error('Error loading occupation list:', error);
  420. occupationList.value = [];
  421. }
  422. }
  423. function handleChange(value: any) {
  424. formData.value = { ...formData.value, [value.key]: value.value };
  425. if (value.key === "nationality") {
  426. formData.value = { ...formData.value, country: value.value };
  427. getCityListForSelect(value.value);
  428. loadIdTypesConfig(value.value);
  429. formData.value.town = "";
  430. }
  431. }
  432. const containsChinese = (str: string) => /[\u4E00-\u9FA5]/.test(str);
  433. const convertToPinyin = (value: string) =>
  434. containsChinese(value)
  435. ? pinyin(value, { toneType: "none", type: "capitalize" })
  436. : value;
  437. // 初始化表单数据 - 使用现有用户信息进行回显
  438. function initFormDataWithUserInfo() {
  439. if (!userInfo.value) return;
  440. const customInfo = userInfo.value.customInfo || userInfo.value;
  441. // 基本信息
  442. formData.value.email = customInfo.email || formData.value.email;
  443. formData.value.areaCode = customInfo.areaCode || formData.value.areaCode;
  444. formData.value.mobile = customInfo.mobile || formData.value.mobile;
  445. formData.value.birthday = customInfo.birthday || formData.value.birthday;
  446. formData.value.gender = customInfo.gender || formData.value.gender;
  447. // 姓名处理
  448. if (customInfo.lastName) {
  449. formData.value.lastName = convertToPinyin(customInfo.lastName);
  450. }
  451. if (customInfo.firstName) {
  452. formData.value.firstName = convertToPinyin(customInfo.firstName);
  453. }
  454. // 地址信息
  455. formData.value.nationality = customInfo.nationality || formData.value.nationality;
  456. formData.value.town = customInfo.town || formData.value.town;
  457. formData.value.address = customInfo.address || formData.value.address;
  458. formData.value.postCode = customInfo.postCode || formData.value.postCode;
  459. // 职业信息
  460. formData.value.occupation = customInfo.occupation || formData.value.occupation;
  461. formData.value.annualSalary = customInfo.annualSalary || formData.value.annualSalary;
  462. formData.value.accountPurpose = customInfo.accountPurpose || formData.value.accountPurpose;
  463. formData.value.expectedMonthlyVolume = customInfo.expectedMonthlyVolume || formData.value.expectedMonthlyVolume;
  464. // 证件信息
  465. formData.value.idType = customInfo.idType || formData.value.idType;
  466. formData.value.idNumber = customInfo.idNumber || formData.value.idNumber;
  467. formData.value.ssn = customInfo.ssn || formData.value.ssn;
  468. formData.value.issueDate = customInfo.issueDate || formData.value.issueDate;
  469. formData.value.idNoExpiryDate = customInfo.idNoExpiryDate || formData.value.idNoExpiryDate;
  470. formData.value.idFrontUrl = customInfo.idFrontUrl || formData.value.idFrontUrl;
  471. formData.value.idBackUrl = customInfo.idBackUrl || formData.value.idBackUrl;
  472. formData.value.idHoldUrl = customInfo.idHoldUrl || formData.value.idHoldUrl;
  473. // 状态信息
  474. formData.value.photoStatus = customInfo.photoStatus || '1';
  475. formData.value.kycStatus = customInfo.kycStatus || '1';
  476. // 对于下拉选择字段,需要确保它们在选项列表中存在
  477. setTimeout(() => {
  478. // 确保 areaCode 在选项中
  479. if (formData.value.areaCode && phoneCodes.value.length > 0) {
  480. const areaCodeExists = phoneCodes.value.some(item => item.value === formData.value.areaCode);
  481. if (!areaCodeExists) {
  482. // 如果不存在,添加该项到选项中
  483. phoneCodes.value.push({
  484. text: formData.value.areaCode,
  485. value: formData.value.areaCode
  486. });
  487. }
  488. }
  489. // 确保 nationality 在选项中
  490. if (formData.value.nationality && countryOptions.value.length > 0) {
  491. const nationalityExists = countryOptions.value.some(item => item.value === formData.value.nationality);
  492. if (!nationalityExists) {
  493. countryOptions.value.push({
  494. text: formData.value.nationality,
  495. value: formData.value.nationality
  496. });
  497. }
  498. }
  499. // 确保 occupation 在选项中
  500. if (formData.value.occupation && occupationList.value.length > 0) {
  501. const occupationExists = occupationList.value.some(item => item.value === formData.value.occupation);
  502. if (!occupationExists) {
  503. occupationList.value.push({
  504. text: formData.value.occupation,
  505. value: formData.value.occupation
  506. });
  507. }
  508. }
  509. // 确保 gender 在选项中
  510. if (formData.value.gender) {
  511. const genderExists = sexOptions.value.some(item => item.value === formData.value.gender);
  512. if (!genderExists) {
  513. sexOptions.value.push({
  514. text: formData.value.gender === 'M' ? t("card.Form.v1") : t("card.Form.v2"),
  515. value: formData.value.gender
  516. });
  517. }
  518. }
  519. }, 100);
  520. }
  521. async function getUserSingle() {
  522. if (!userToken.value) {
  523. uni.showToast({ title: t("common.loginFirst"), icon: 'none' });
  524. return;
  525. }
  526. try {
  527. const res = await userApi.getUserSingle();
  528. if (res.code === 200 && res.data) {
  529. // 更新表单数据
  530. formData.value = { ...formData.value, ...res.data };
  531. userStore.saveUserInfo(res.data);
  532. // 设置状态
  533. if (res.data) {
  534. isUpdate.value = true;
  535. }
  536. if (res.data.approveStatus == 2 || res.data.kycStatus == 2) {
  537. isAuthInfo.value = true;
  538. }
  539. if (res.data.approveStatus == 3) {
  540. isAuthInfo.value = false;
  541. }
  542. // 如果有国籍信息,加载对应的城市列表
  543. if (res.data.nationality) {
  544. await getCityListForSelect(res.data.nationality);
  545. await loadIdTypesConfig(res.data.nationality);
  546. }
  547. }
  548. } catch (error: any) {
  549. console.error('Error in getUserSingle:', error);
  550. uni.showToast({ title: error.message || t("common.error"), icon: 'none' });
  551. }
  552. }
  553. // 加载状态
  554. const loadingStates = ref({
  555. validateStep1: false,
  556. validateStep2: false,
  557. validateStep3: false,
  558. validateStep4: false,
  559. });
  560. // 第一步校验 - 基本信息完整性
  561. async function validateStep1() {
  562. loadingStates.value.validateStep1 = true;
  563. try {
  564. const requiredFields = [
  565. "email",
  566. "lastName",
  567. "firstName",
  568. "mobile",
  569. "areaCode",
  570. "birthday",
  571. "gender",
  572. ] as const;
  573. await formRef.value?.validateField(requiredFields, (errorsRes: any) => {
  574. const hasError = Array.isArray(errorsRes) && errorsRes.length > 0;
  575. if (hasError) {
  576. uni.showToast({ title: errorsRes[0].message, icon: 'none' });
  577. } else {
  578. goStep(2);
  579. }
  580. });
  581. } catch (error: any) {
  582. if (Array.isArray(error) && error.length > 0) {
  583. uni.showToast({ title: error[0].message, icon: 'none' });
  584. } else {
  585. }
  586. } finally {
  587. loadingStates.value.validateStep1 = false;
  588. }
  589. }
  590. // 第二步校验 - 国籍信息
  591. async function validateStep2() {
  592. loadingStates.value.validateStep2 = true;
  593. try {
  594. const requiredFields = [
  595. "nationality",
  596. "town",
  597. "postCode",
  598. "address",
  599. ] as const;
  600. await formRef.value?.validateField(requiredFields, (errorsRes: any) => {
  601. const hasError = Array.isArray(errorsRes) && errorsRes.length > 0;
  602. if (hasError) {
  603. uni.showToast({ title: errorsRes[0].message, icon: 'none' });
  604. } else {
  605. goStep(3);
  606. }
  607. });
  608. } catch (error: any) {
  609. if (Array.isArray(error) && error.length > 0) {
  610. uni.showToast({ title: error[0].message, icon: 'none' });
  611. } else {
  612. }
  613. } finally {
  614. loadingStates.value.validateStep2 = false;
  615. }
  616. }
  617. // 第三步校验 - 职业信息
  618. async function validateStep3() {
  619. loadingStates.value.validateStep3 = true;
  620. try {
  621. const requiredFields = [
  622. "occupation",
  623. "annualSalary",
  624. "accountPurpose",
  625. "expectedMonthlyVolume",
  626. ] as const;
  627. await formRef.value?.validateField(requiredFields, (errorsRes: any) => {
  628. const hasError = Array.isArray(errorsRes) && errorsRes.length > 0;
  629. if (hasError) {
  630. uni.showToast({ title: errorsRes[0].message, icon: 'none' });
  631. } else {
  632. goStep(4);
  633. }
  634. });
  635. } catch (error: any) {
  636. if (Array.isArray(error) && error.length > 0) {
  637. uni.showToast({ title: error[0].message, icon: 'none' });
  638. } else {
  639. }
  640. } finally {
  641. loadingStates.value.validateStep3 = false;
  642. }
  643. }
  644. // 第四步校验 校验所有信息
  645. async function validateStep4(type: number) {
  646. loadingStates.value.validateStep4 = true;
  647. try {
  648. const requiredFields: Array<keyof typeof formData.value> = [
  649. "email",
  650. "lastName",
  651. "firstName",
  652. "mobile",
  653. "areaCode",
  654. "nationality",
  655. "town",
  656. "postCode",
  657. "address",
  658. "birthday",
  659. "gender",
  660. "idType",
  661. "idNumber",
  662. "issueDate",
  663. "idNoExpiryDate",
  664. "occupation",
  665. "annualSalary",
  666. "accountPurpose",
  667. "expectedMonthlyVolume",
  668. ];
  669. // 只有在需要上传照片时才验证照片字段
  670. if (formData.value.photoStatus === '1') {
  671. requiredFields.push("idFrontUrl", "idBackUrl", "idHoldUrl");
  672. }
  673. await formRef.value?.validateField(requiredFields, (errorsRes: any) => {
  674. const hasError = Array.isArray(errorsRes) && errorsRes.length > 0;
  675. console.log(hasError, 121212);
  676. if (hasError) {
  677. uni.showToast({ title: errorsRes[0].message, icon: 'none' });
  678. } else {
  679. // 最终校验所有信息
  680. switch (type) {
  681. case 1:
  682. infoSubmit();
  683. break;
  684. case 2:
  685. infoUpdate();
  686. break;
  687. case 3:
  688. infoAuth();
  689. break;
  690. default:
  691. break;
  692. }
  693. }
  694. });
  695. } catch (error: any) {
  696. if (Array.isArray(error) && error.length > 0) {
  697. uni.showToast({ title: error[0].message, icon: 'none' });
  698. } else {
  699. }
  700. } finally {
  701. loadingStates.value.validateStep4 = false;
  702. }
  703. }
  704. async function infoSubmit() {
  705. try {
  706. const res = await ucardApi.merchantRegister(formData.value);
  707. if (res.code === 200) {
  708. uni.showToast({ title: t("common.success"), icon: 'success' });
  709. // 提交成功后打开 WebSDK 弹窗
  710. router.push({
  711. path: "/pages/mine/kyc",
  712. query: { cardId: (formData.value as any).id },
  713. });
  714. // cardWebsdkLinkRef.value?.getWebsdkLink(
  715. // (formData.value as any).id
  716. // );
  717. } else {
  718. uni.showToast({ title: res.msg, icon: 'none' });
  719. }
  720. } catch (error: any) {
  721. uni.showToast({ title: error.message || t("common.error"), icon: 'none' });
  722. }
  723. }
  724. async function infoUpdate() {
  725. try {
  726. const res = await ucardApi.merchantUpdate(formData.value);
  727. if (res.code === 200) {
  728. uni.showToast({ title: t("common.success"), icon: 'success' });
  729. // 更新成功后打开 WebSDK 弹窗
  730. // router.push({
  731. // path: "/pages/mine/kyc",
  732. // query: { cardId: (formData.value as any).id },
  733. // });
  734. } else {
  735. uni.showToast({ title: res.msg, icon: 'none' });
  736. }
  737. } catch (error: any) {
  738. if (Array.isArray(error) && error.length > 0) {
  739. uni.showToast({ title: error[0].message, icon: 'none' });
  740. } else {
  741. uni.showToast({ title: t("card.New.errer"), icon: 'none' });
  742. }
  743. }
  744. }
  745. async function infoAuth() {
  746. try {
  747. // 认证操作后也打开 WebSDK 弹窗(无需等待额外接口)
  748. router.push({
  749. path: "/pages/mine/kyc",
  750. query: { cardId: (formData.value as any).id },
  751. });
  752. } catch (error: any) {
  753. if (Array.isArray(error) && error.length > 0) {
  754. uni.showToast({ title: error[0].message, icon: 'none' });
  755. } else {
  756. uni.showToast({ title: t("card.New.errer"), icon: 'none' });
  757. }
  758. }
  759. }
  760. const isReadonly = computed(() => isAuthInfo.value);
  761. onMounted(async () => {
  762. // 用现有用户信息回显
  763. initFormDataWithUserInfo();
  764. // 先加载选项数据
  765. await Promise.all([
  766. getOccupationList(),
  767. getCountryListForSelect()
  768. ]);
  769. // 延迟加载用户详细信息
  770. setTimeout(async () => {
  771. await getUserSingle();
  772. }, 100);
  773. });
  774. </script>
  775. <style scoped lang="scss">
  776. @import "@/uni.scss";
  777. .form-tab {
  778. height: px2rpx(100);
  779. }
  780. .form-section {
  781. margin: px2rpx(8) 0;
  782. }
  783. .section-title {
  784. color: #1a1a1a;
  785. font-family: Roboto;
  786. font-size: px2rpx(22);
  787. font-style: normal;
  788. font-weight: 600;
  789. line-height: px2rpx(22);
  790. margin-bottom: px2rpx(16);
  791. }
  792. .two-btn {
  793. display: flex;
  794. align-items: center;
  795. gap: px2rpx(31);
  796. .prev-btn {
  797. border: 1px solid var(--main-yellow) !important;
  798. color: #fff !important;
  799. background: transparent;
  800. }
  801. }
  802. .f {
  803. display: flex;
  804. align-items: flex-end;
  805. gap: px2rpx(12);
  806. .l {
  807. flex: 1;
  808. }
  809. .r {
  810. width: px2rpx(203);
  811. }
  812. }
  813. :deep(.u-uploader) {
  814. width: 100%;
  815. .u-uploader__wrapper {
  816. width: 100%;
  817. display: block;
  818. }
  819. .u-uploader__preview {
  820. width: 100% !important;
  821. height: px2rpx(160) !important;
  822. border-radius: px2rpx(24);
  823. overflow: hidden;
  824. .u-uploader__preview-image {
  825. width: 100%;
  826. height: 100%;
  827. .u-image__img {
  828. object-fit: contain;
  829. }
  830. }
  831. .u-uploader__preview-delete {
  832. position: absolute;
  833. top: 0;
  834. right: 0;
  835. width: px2rpx(30);
  836. height: px2rpx(30);
  837. border-radius: 0 px2rpx(24) 0 0;
  838. i {
  839. text-align: center;
  840. line-height: px2rpx(30);
  841. font-size: px2rpx(30);
  842. }
  843. }
  844. }
  845. }
  846. </style>