improve.vue 32 KB

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