improve.vue 32 KB

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