improve-info.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  1. <template>
  2. <div class="page">
  3. <div class="kyc-form" v-if="isShow">
  4. <remit-input
  5. v-model:value="infoForm.lastName"
  6. fkey="lastName"
  7. :required="true"
  8. :label="t('improve-info.p8')"
  9. :rules="rules['lastName']"
  10. @change="handleChange"
  11. />
  12. <remit-input
  13. v-model:value="infoForm.firstName"
  14. fkey="firstName"
  15. :required="true"
  16. :label="t('improve-info.p9')"
  17. :rules="rules['firstName']"
  18. @change="handleChange"
  19. />
  20. <remit-input
  21. v-model:value="infoForm.email"
  22. fkey="email"
  23. :label="t('improve-info.p7')"
  24. :required="true"
  25. :rules="rules['email']"
  26. @change="handleChange"
  27. />
  28. <remit-input
  29. v-model:value="infoForm.areaCode"
  30. fkey="areaCode"
  31. :required="true"
  32. type="select"
  33. :show-search="true"
  34. :columns="phoneCodes"
  35. :label="t('improve-info.p4')"
  36. :rules="rules['areaCode']"
  37. @change="handleChange"
  38. />
  39. <remit-input
  40. v-model:value="infoForm.mobile"
  41. fkey="mobile"
  42. :label="t('improve-info.p6')"
  43. :required="true"
  44. :rules="rules['mobile']"
  45. @change="handleChange"
  46. />
  47. <remit-input
  48. v-model:value="infoForm.sex"
  49. fkey="sex"
  50. type="select"
  51. :columns="sexOptions"
  52. :label="t('improve-info.p13')"
  53. @change="handleChange"
  54. />
  55. <remit-input v-model:value="infoForm.birthday" type="date" fkey="birthday" :label="t('improve-info.p10')" @change="handleChange" />
  56. <remit-input
  57. v-model:value="infoForm.nationality"
  58. fkey="nationality"
  59. type="select"
  60. :show-search="true"
  61. :columns="countryOptions"
  62. :label="t('improve-info.p17')"
  63. @change="handleChange"
  64. />
  65. <remit-input
  66. v-model:value="infoForm.town"
  67. fkey="town"
  68. :show-search="true"
  69. type="select"
  70. :columns="cityOptions"
  71. :label="t('improve-info.p19')"
  72. @change="handleChange"
  73. />
  74. <remit-input
  75. v-model:value="infoForm.address"
  76. fkey="address"
  77. :label="t('improve-info.p21')"
  78. :required="true"
  79. :rules="rules['address']"
  80. @change="handleChange"
  81. />
  82. <remit-input v-model:value="infoForm.postCode" fkey="postCode" :label="t('improve-info.p22')" @change="handleChange" />
  83. <remit-input
  84. v-model:value="infoForm.idType"
  85. fkey="idType"
  86. type="select"
  87. :columns="idTypeOptions"
  88. :label="t('improve-info.p23')"
  89. @change="handleChange"
  90. />
  91. <remit-input v-model:value="infoForm.idNo" fkey="idNo" :label="t('improve-info.p27')" @change="handleChange" />
  92. <remit-input
  93. v-model:value="infoForm.idNoExpiryDate"
  94. type="date"
  95. fkey="idNoExpiryDate"
  96. :label="t('improve-info.p28')"
  97. @change="handleChange"
  98. />
  99. <remit-input v-model:value="infoForm.idPicture" type="upload" fkey="idPicture" :label="t('improve-info.p29')" @change="handleChange" />
  100. <remit-input
  101. v-model:value="infoForm.facePicture"
  102. type="upload"
  103. fkey="facePicture"
  104. :label="t('improve-info.p30')"
  105. @change="handleChange"
  106. />
  107. <!-- <div class="form-item phone-input">
  108. <div :class="{ 'phone-code': true, 'phone-code-active': form.phoneCode }">
  109. <van-field
  110. is-link
  111. readonly
  112. @click="showPicker = true"
  113. v-model="form.phoneCode"
  114. :placeholder="t('improve-info.phoneCode')"
  115. :rules="[{ required: true, message: t('improve-info.phoneCode') }]"
  116. autocomplete="off"
  117. />
  118. <van-popup v-model:show="showPicker" destroy-on-close round position="bottom">
  119. <van-picker
  120. v-model="form.phoneCode1"
  121. :visible-option-num="8"
  122. show-toolbar
  123. :title="t('improve-info.country')"
  124. :columns="[phoneCodes]"
  125. :cancel-button-text="t('common.cancel')"
  126. :confirm-button-text="t('common.confirm')"
  127. @cancel="showPicker = false"
  128. @confirm="onConfirm"
  129. />
  130. </van-popup>
  131. </div>
  132. <div class="phone-divider"></div>
  133. <div class="phone-number">
  134. <van-field
  135. v-model="form.phone"
  136. :placeholder="t('improve-info.phone')"
  137. :rules="[{ required: true, message: t('improve-info.phone') }]"
  138. autocomplete="off"
  139. />
  140. </div>
  141. </div> -->
  142. <div class="kyc-button">
  143. <van-button type="primary" block @click="kycSubmit">{{ t('improve-info.p33') }}</van-button>
  144. </div>
  145. </div>
  146. </div>
  147. </template>
  148. <script setup lang="ts">
  149. import { pinyin } from 'pinyin-pro'
  150. import { useRouter } from 'vue-router'
  151. import { useI18n } from 'vue-i18n'
  152. import { showToast } from 'vant'
  153. import { ucardApi } from '@/api/ucard'
  154. import { userApi } from '@/api/user'
  155. import useUserStore from '@/stores/use-user-store'
  156. const userStore = useUserStore()
  157. const userInfo = computed(() => userStore.userInfo)
  158. const { t } = useI18n()
  159. const router = useRouter()
  160. const sexOptions = ref([
  161. { text: t('improve-info.p15'), value: 1 },
  162. { text: t('improve-info.p16'), value: 2 },
  163. ])
  164. const idTypeOptions = ref([
  165. { text: t('improve-info.p25'), value: 'EUROPEAN_ID' },
  166. { text: t('improve-info.p26'), value: 'PASSPORT' },
  167. ])
  168. // 表单验证规则
  169. const rules = {
  170. email: [{ required: true, message: t('improve-info.p3') }],
  171. lastName: [{ required: true, message: t('improve-info.p3') }],
  172. firstName: [{ required: true, message: t('improve-info.p3') }],
  173. address: [{ required: true, message: t('improve-info.p3') }],
  174. mobile: [{ required: true, message: t('improve-info.p3') }],
  175. areaCode: [{ required: true, message: t('improve-info.p5') }],
  176. }
  177. const infoForm = ref({
  178. areaCode: undefined,
  179. mobile: undefined,
  180. idNo: undefined,
  181. sex: undefined,
  182. birthday: undefined,
  183. nationality: undefined,
  184. town: undefined,
  185. postCode: undefined,
  186. idType: undefined,
  187. idNoExpiryDate: undefined,
  188. idPicture: undefined,
  189. facePicture: undefined,
  190. email: undefined,
  191. lastName: undefined,
  192. firstName: undefined,
  193. address: undefined,
  194. cId: undefined,
  195. })
  196. const formData = ref<typeof infoForm.value>({} as any)
  197. const isShow = ref(false)
  198. // 国家选项
  199. const countryOptions = ref<Array<{ text: string; value: string }>>([])
  200. const cityOptions = ref<Array<{ text: string; value: string }>>([])
  201. const phoneCodes = ref([])
  202. // 获取国家列表
  203. const getCountryListForSelect = async () => {
  204. try {
  205. const res = await ucardApi.ucardCountryCity({})
  206. if (res.code === 200 || res.code === 0) {
  207. countryOptions.value = res.data.map((item: any) => ({
  208. text: lang.value === 'cn' ? item.cnName : item.enName,
  209. value: item.code,
  210. }))
  211. }
  212. } catch (error) {
  213. countryOptions.value = []
  214. }
  215. }
  216. // 获取城市列表
  217. const getCityListForSelect = async (countryCode: string) => {
  218. try {
  219. const res = await ucardApi.ucardCountryCity({ code: countryCode })
  220. if (res.code === 200 || res.code === 0) {
  221. const cityList = res.data.map((item: any) => ({
  222. text: lang.value === 'cn' ? item.cnName : item.enName,
  223. value: item.code,
  224. }))
  225. cityOptions.value = cityList
  226. isShow.value = true
  227. }
  228. } catch (error) {
  229. cityOptions.value = []
  230. }
  231. }
  232. const getCountry = async () => {
  233. try {
  234. const res = await ucardApi.countryGet()
  235. phoneCodes.value = res.data.map((item: { callingCode: string; name: string; enName: string }) => ({
  236. text: lang.value === 'cn' ? item.name + ' + ' + item.callingCode : item.enName + ' + ' + item.callingCode,
  237. value: item.callingCode,
  238. }))
  239. } catch (error) {
  240. showToast(error || String(error))
  241. }
  242. }
  243. const kycSubmit = async () => {
  244. const requiredFields = ['email', 'lastName', 'firstName', 'address', 'mobile', 'areaCode'] as const
  245. const missingFields = requiredFields.filter((field) => !formData.value[field])
  246. if (missingFields.length > 0) {
  247. showToast(t('eur-remit.requiredFieldsMissing'))
  248. return
  249. }
  250. let res
  251. if (formData.value.uniqueId) {
  252. res = await ucardApi.merchantUpdate(formData.value as any)
  253. if (res.code === 200) {
  254. showToast(t('improve-info.kycSuccess'))
  255. }
  256. } else {
  257. res = await ucardApi.merchantRegister(formData.value as any)
  258. if (res.code === 200) {
  259. showToast(t('improve-info.kycSuccess'))
  260. userInfo.value.customInfo.uniqueId = res.data
  261. console.log(userInfo.value)
  262. userStore.saveUserInfo(userInfo.value)
  263. router.push('/')
  264. }
  265. }
  266. }
  267. const handleChange = (value: any) => {
  268. formData.value = { ...formData.value, [value.key]: value.value }
  269. if (value.key === 'nationality') {
  270. getCityListForSelect(value.value)
  271. formData.value.town = ''
  272. }
  273. }
  274. const containsChinese = (str: string) => /[\u4e00-\u9fa5]/.test(str)
  275. const convertToPinyin = (value: string) => (containsChinese(value) ? pinyin(value, { toneType: 'none', type: 'capitalize' }) : value)
  276. const getInfoForm = () => {
  277. const customInfo = userInfo.value?.customInfo || {}
  278. const fields = [
  279. 'areaCode',
  280. 'mobile',
  281. 'idNo',
  282. 'sex',
  283. 'birthday',
  284. 'nationality',
  285. 'town',
  286. 'postCode',
  287. 'idType',
  288. 'idNoExpiryDate',
  289. 'idPicture',
  290. 'facePicture',
  291. 'email',
  292. 'lastName',
  293. 'firstName',
  294. 'address',
  295. 'cId',
  296. 'uniqueId',
  297. ]
  298. const data: Record<string, any> = {}
  299. for (const key of fields) {
  300. data[key] = customInfo[key] ?? undefined
  301. }
  302. data.lastName = convertToPinyin(data.lastName)
  303. data.firstName = convertToPinyin(data.firstName)
  304. infoForm.value = { ...data }
  305. formData.value = { ...formData.value, ...data }
  306. }
  307. const getUserInfo = async () => {
  308. if (!userToken.value) {
  309. showToast('请先登录')
  310. return
  311. }
  312. try {
  313. const res = await userApi.getUserInfo()
  314. if (res.code === 200) {
  315. if (res.data.customInfo.uniqueId) {
  316. router.push('/')
  317. } else {
  318. router.push('/improve/info')
  319. }
  320. } else {
  321. showToast(res.msg || '登录失败')
  322. }
  323. } catch (error: any) {
  324. showToast(error.message || '登录失败')
  325. } finally {
  326. }
  327. }
  328. onMounted(() => {
  329. getInfoForm()
  330. isShow.value = false
  331. getCountryListForSelect()
  332. getCountry()
  333. if (infoForm.value.nationality) {
  334. getCityListForSelect(infoForm.value.nationality)
  335. } else {
  336. isShow.value = true
  337. }
  338. })
  339. </script>
  340. <style scoped lang="scss">
  341. .kyc-page {
  342. min-height: 100vh;
  343. background: linear-gradient(135deg, #232323 0%, #2a2a2a 100%);
  344. color: #fff;
  345. display: flex;
  346. flex-direction: column;
  347. align-items: center;
  348. padding: 32px 12px;
  349. }
  350. .kyc-header {
  351. width: 100%;
  352. text-align: center;
  353. margin-bottom: 24px;
  354. h2 {
  355. font-size: var(--font-size-18);
  356. color: var(--main-yellow);
  357. margin-bottom: 8px;
  358. }
  359. p {
  360. color: #bbb;
  361. font-size: var(--font-size-12);
  362. line-height: 1.5;
  363. }
  364. }
  365. .kyc-form {
  366. width: 100%;
  367. max-width: 350px;
  368. border-radius: 20px;
  369. padding: 28px 18px 18px 18px;
  370. display: flex;
  371. flex-direction: column;
  372. gap: 18px;
  373. }
  374. .form-item {
  375. margin-bottom: 0;
  376. ::v-deep {
  377. .van-field {
  378. background: #181818;
  379. border-radius: 12px;
  380. padding: 12px 16px;
  381. transition: all 0.3s ease;
  382. &:focus-within {
  383. background: #1f1f1f;
  384. }
  385. .van-field__label {
  386. color: #bbb;
  387. font-size: var(--font-size-14);
  388. width: 80px;
  389. }
  390. .van-field__control {
  391. color: #fff;
  392. font-size: var(--font-size-14);
  393. min-height: 20px;
  394. line-height: 20px;
  395. &::placeholder {
  396. color: #666;
  397. font-size: var(--font-size-14);
  398. }
  399. &::-webkit-input-placeholder {
  400. color: #666;
  401. }
  402. &::-moz-placeholder {
  403. color: #666;
  404. opacity: 1;
  405. }
  406. }
  407. .van-field__right-icon {
  408. color: var(--main-yellow);
  409. margin-left: 8px;
  410. }
  411. .van-field__error-message {
  412. color: #ff4d4f;
  413. font-size: var(--font-size-12);
  414. margin-top: 4px;
  415. }
  416. }
  417. }
  418. }
  419. .kyc-button {
  420. margin-top: 18px;
  421. ::v-deep {
  422. .van-button {
  423. height: 44px;
  424. border-radius: 24px;
  425. background: var(--main-yellow);
  426. border: none;
  427. color: #232323;
  428. font-size: var(--font-size-16);
  429. font-weight: bold;
  430. &:active {
  431. opacity: 0.9;
  432. }
  433. }
  434. }
  435. }
  436. .apply-card-steps {
  437. width: 100%;
  438. max-width: 350px;
  439. margin-bottom: 32px;
  440. .steps-top {
  441. display: flex;
  442. align-items: center;
  443. justify-content: space-between;
  444. padding: 0 12px;
  445. }
  446. .step {
  447. display: flex;
  448. flex-direction: column;
  449. align-items: center;
  450. gap: 8px;
  451. .step-circle {
  452. width: 32px;
  453. height: 32px;
  454. border-radius: 50%;
  455. background: var(--action-bg, #181818);
  456. border: 2px solid #444;
  457. color: #444;
  458. display: flex;
  459. align-items: center;
  460. justify-content: center;
  461. font-size: var(--font-size-16);
  462. font-weight: bold;
  463. transition: all 0.3s ease;
  464. }
  465. .step-label {
  466. font-size: var(--font-size-14);
  467. color: #444;
  468. transition: all 0.3s ease;
  469. }
  470. &.active {
  471. .step-circle {
  472. background: var(--main-yellow);
  473. border-color: var(--main-yellow);
  474. color: #232323;
  475. }
  476. .step-label {
  477. color: var(--main-yellow);
  478. }
  479. }
  480. }
  481. .step-dash {
  482. flex: 1;
  483. height: 2px;
  484. margin: 0 8px;
  485. position: relative;
  486. top: -16px;
  487. transition: all 0.3s ease;
  488. background-image: linear-gradient(to right, #444 50%, transparent 50%);
  489. background-size: 8px 2px;
  490. background-repeat: repeat-x;
  491. &.active {
  492. background-image: linear-gradient(to right, var(--main-yellow) 50%, transparent 50%);
  493. }
  494. }
  495. }
  496. .phone-input {
  497. display: flex;
  498. align-items: center;
  499. background: #181818;
  500. border-radius: 12px;
  501. padding: 0 16px;
  502. height: 44px;
  503. transition: all 0.3s ease;
  504. &:focus-within {
  505. background: #1f1f1f;
  506. }
  507. .phone-code {
  508. width: 20%;
  509. ::v-deep .van-cell {
  510. &::after {
  511. border-bottom: 0;
  512. }
  513. .van-icon {
  514. font-size: var(--font-size-14);
  515. line-height: 20px;
  516. }
  517. }
  518. ::v-deep {
  519. .van-field {
  520. background: transparent;
  521. padding-left: 0;
  522. padding-right: 0;
  523. .van-field__control {
  524. color: var(--white);
  525. text-align: center;
  526. padding: 0;
  527. }
  528. }
  529. }
  530. }
  531. .phone-code-active {
  532. ::v-deep .van-cell {
  533. .van-icon {
  534. color: var(--white);
  535. }
  536. }
  537. }
  538. .phone-divider {
  539. width: 1px;
  540. height: 20px;
  541. background: #333;
  542. margin: 0 12px;
  543. transition: all 0.3s ease;
  544. }
  545. .phone-number {
  546. flex: 1;
  547. ::v-deep {
  548. .van-field {
  549. background: transparent;
  550. padding-left: 10px;
  551. .van-field__control {
  552. color: #fff;
  553. text-align: left;
  554. padding: 0;
  555. &::placeholder {
  556. color: #666;
  557. text-align: left;
  558. }
  559. }
  560. }
  561. }
  562. }
  563. }
  564. @keyframes slideDown {
  565. from {
  566. opacity: 0;
  567. transform: translateY(-10px);
  568. }
  569. to {
  570. opacity: 1;
  571. transform: translateY(0);
  572. }
  573. }
  574. @keyframes fadeIn {
  575. from {
  576. opacity: 0;
  577. }
  578. to {
  579. opacity: 0.2;
  580. }
  581. }
  582. ::v-deep {
  583. .van-picker {
  584. background: #181818;
  585. .van-picker__toolbar {
  586. background: #181818;
  587. border-bottom: 1px solid #333;
  588. height: 44px;
  589. padding: 0 16px;
  590. .van-picker__title {
  591. color: #fff;
  592. font-size: var(--font-size-16);
  593. font-weight: 500;
  594. }
  595. .van-picker__cancel,
  596. .van-picker__confirm {
  597. color: var(--main-yellow);
  598. font-size: var(--font-size-14);
  599. padding: 0 8px;
  600. height: 28px;
  601. line-height: 28px;
  602. border-radius: 14px;
  603. transition: all 0.3s ease;
  604. &:active {
  605. opacity: 0.8;
  606. background: rgba(255, 193, 7, 0.1);
  607. }
  608. }
  609. }
  610. .van-picker-column {
  611. color: #fff;
  612. .van-picker-column__item {
  613. color: #fff;
  614. font-size: var(--font-size-14);
  615. padding: 0 16px;
  616. height: 44px;
  617. line-height: 44px;
  618. transition: all 0.3s ease;
  619. &--selected {
  620. color: var(--main-yellow);
  621. font-weight: 500;
  622. font-size: var(--font-size-16);
  623. }
  624. &:active {
  625. background: rgba(255, 255, 255, 0.05);
  626. }
  627. }
  628. .van-picker-column__wrapper {
  629. &::after {
  630. border-color: #333;
  631. }
  632. }
  633. }
  634. .van-picker__mask {
  635. background-image: linear-gradient(180deg, rgba(24, 24, 24, 0.9), rgba(24, 24, 24, 0.4)),
  636. linear-gradient(0deg, rgba(24, 24, 24, 0.9), rgba(24, 24, 24, 0.4));
  637. }
  638. .van-picker__indicator {
  639. height: 44px;
  640. background: rgba(255, 193, 7, 0.05);
  641. border-top: 1px solid rgba(255, 193, 7, 0.1);
  642. border-bottom: 1px solid rgba(255, 193, 7, 0.1);
  643. }
  644. }
  645. }
  646. </style>