info.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. <template>
  2. <cwg-page-wrapper>
  3. <uni-tabs v-model="current" :tabs="tabs" @change="changeTab" color="#333" lineHeight="0" field="name">
  4. <template v-slot="{ row, index }">
  5. <view class="tab-title " :class="{ active: current == row.id }">
  6. <cwg-icon v-if="row.icon" :name="row.icon" :size="16"
  7. :color="current == row.id ? '#fff' : '#333'" />
  8. <text>{{ row.name }}</text>
  9. </view>
  10. </template>
  11. </uni-tabs>
  12. <view class="info-card">
  13. <view class="content-title" v-if="current != 3 && current != 4">
  14. <view>{{ tabs[current - 1].name }}</view>
  15. <view class="content-title-btns">
  16. <view v-if="current == 3 && !isSHowIdentity" class="btn-primary" @click="infoSubmit()">
  17. <cwg-icon icon="crm-plus" :size="16" color="#fff" />
  18. <text v-t="'PersonalManagement.Title.ProofOfIdentity'"></text>
  19. </view>
  20. <view v-if="current == 3 && !isSHowAddress" class="btn-primary" @click="cancle()">
  21. <cwg-icon icon="crm-plus" :size="16" color="#fff" />
  22. <text v-t="'PersonalManagement.Title.ProofOfAddress'"></text>
  23. </view>
  24. <view v-if="current == 3" class="btn-primary" @click="cancle()">
  25. <cwg-icon icon="crm-plus" :size="16" color="#fff" />
  26. <text v-t="'PersonalManagement.Title.AttachedFile'"></text>
  27. </view>
  28. </view>
  29. </view>
  30. <view v-if="current == 1">
  31. <personal-info-tab v-model="PersonalInformation" :is-readonly="isReadonly" @cancel="cancle"
  32. @next="next(2)" />
  33. </view>
  34. <view v-if="current == 2">
  35. <bank-info-tab />
  36. </view>
  37. <view v-if="current == 3">
  38. <file-management-tab />
  39. </view>
  40. <view v-if="current == 4">
  41. <security-center-tab />
  42. </view>
  43. </view>
  44. <!-- <view class="form-tab"></view> -->
  45. </cwg-page-wrapper>
  46. <card-websdk-link ref="cardWebsdkLinkRef" />
  47. </template>
  48. <script setup lang="ts">
  49. import { ref, onMounted, watch, computed } from "vue";
  50. import { pinyin } from "pinyin-pro";
  51. import { useI18n } from "vue-i18n";
  52. import { onLoad } from '@dcloudio/uni-app'
  53. import { userToken } from "@/composables/config";
  54. import { ucardApi } from "@/api/ucard";
  55. import { personalApi } from "@/service/personal";
  56. import { userApi } from "@/api/user";
  57. import useUserStore from "@/stores/use-user-store";
  58. import useIdTypeOptions from "@/composables/useIdTypeOptions";
  59. import CardWebsdkLink from "@/components/card-websdkLink.vue";
  60. import PersonalInfoTab from "./components/PersonalInfoTab.vue";
  61. import FileManagementTab from "./components/FileManagementTab.vue";
  62. import BankInfoTab from "./components/BankInfoTab.vue";
  63. import SecurityCenterTab from "./components/SecurityCenterTab.vue";
  64. import useRouter from "@/hooks/useRouter";
  65. const notCountry = [
  66. "AF",
  67. "AI",
  68. "AG",
  69. "BS",
  70. "BY",
  71. "BZ",
  72. "BA",
  73. "BI",
  74. "CF",
  75. "CD",
  76. "CU",
  77. "ET",
  78. "FJ",
  79. "PS",
  80. "GN",
  81. "GW",
  82. "HT",
  83. "IR",
  84. "IQ",
  85. "LB",
  86. "LY",
  87. "ML",
  88. "MM",
  89. "NI",
  90. "KP",
  91. "PW",
  92. "RU",
  93. "SO",
  94. "SS",
  95. "SD",
  96. "SY",
  97. "UA",
  98. "US",
  99. "VE",
  100. "YE",
  101. "ZW",
  102. ]
  103. const router = useRouter();
  104. const userStore = useUserStore();
  105. const userInfo = computed(() => userStore.userInfo);
  106. const current = ref(0);
  107. onLoad((options) => {
  108. current.value = options?.type;
  109. });
  110. const tabs = computed(() => [
  111. { id: 1, name: t('PersonalManagement.Title.PersonalInformation'), icon: 'crm-circle-user' },
  112. { id: 2, name: t('PersonalManagement.Title.BankInformation'), icon: 'crm-building-columns' },
  113. { id: 3, name: t('PersonalManagement.Title.FileManagement'), icon: 'crm-file' },
  114. { id: 4, name: t('PersonalManagement.Title.SecurityCenter'), icon: 'crm-lock' }
  115. ]);
  116. const changeTab = (index) => {
  117. router.push(`/pages/mine/info?type=${index + 1}`);
  118. }
  119. const { t, locale } = useI18n();
  120. const cardWebsdkLinkRef = ref<InstanceType<typeof CardWebsdkLink> | null>(null);
  121. const PersonalInformation = ref({
  122. merchantOrderNo: undefined,
  123. cardTypeId: undefined,
  124. areaCode: undefined,
  125. mobile: undefined,
  126. email: undefined,
  127. firstName: undefined,
  128. lastName: undefined,
  129. birthday: undefined,
  130. nationality: undefined,
  131. country: undefined,
  132. town: undefined,
  133. address: undefined,
  134. postCode: undefined,
  135. gender: undefined,
  136. occupation: undefined,
  137. annualSalary: undefined,
  138. accountPurpose: undefined,
  139. expectedMonthlyVolume: undefined,
  140. idType: undefined,
  141. idNumber: undefined,
  142. ssn: undefined,
  143. issueDate: undefined,
  144. idNoExpiryDate: undefined,
  145. idFrontUrl: undefined,
  146. idBackUrl: undefined,
  147. idHoldUrl: undefined,
  148. ipAddress: undefined,
  149. cId: undefined,
  150. customId: undefined,
  151. photoStatus: '1',
  152. kycStatus: '1'
  153. });
  154. const isUpdate = ref(false);
  155. const isAuthInfo = ref(false);
  156. const dialogCheck = ref(false);
  157. const dialogCheck1 = ref(false);
  158. const isApprove = computed(() => [2, 4].includes(userInfo.value.approveStatus))
  159. const countryList = ref<any[]>([]);
  160. const getCountry = async () => {
  161. const res = await personalApi.Country({});
  162. if (res.code === 200) {
  163. countryList.value = res.data || [];
  164. }
  165. };
  166. const cancle = async () => {
  167. if (!isApprove.value) {
  168. dialogCheck.value = true;
  169. dialogCheck1.value = true;
  170. } else {
  171. router.push({ path: "/customer/index" });
  172. }
  173. }
  174. const currentUploadCard = computed(() => {
  175. if (!PersonalInformation.value.nationality || !countryList.value.length) {
  176. return 0;
  177. }
  178. const selectedCountry = countryList.value.find(
  179. (item) => item.code === PersonalInformation.value.nationality
  180. );
  181. return selectedCountry ? (selectedCountry.uploadCard || 0) : 0;
  182. })
  183. const next = async (index) => {
  184. // 从 step 2 跳转到 step 3 时,应该直接跳转,不做验证
  185. // 只有在 step 3 点击下一步时,才检查是否需要跳过到 step 4
  186. if (current.value == 3 && index == 4 && PersonalInformation.value.uploadImage == 0) {
  187. // 如果不需要上传图片,跳过 step 3 直接到 step 4
  188. index = 4;
  189. }
  190. // 从 step 3 跳转到 step 4 时,需要验证身份证明相关字段
  191. if (index == 4) {
  192. // 根据当前选中国籍的 uploadCard 值判断是否需要验证证件照
  193. if (currentUploadCard.value === 1) {
  194. if (!PersonalInformation.value.cardType) {
  195. uni.showToast({ title: t('vaildate.CardType.empty'), icon: 'none' });
  196. return;
  197. }
  198. if (!PersonalInformation.value.fileListID1.path || !PersonalInformation.value.fileListID2.path) {
  199. uni.showToast({ title: t('vaildate.IDPhoto.empty'), icon: 'none' });
  200. return;
  201. }
  202. }
  203. }
  204. router.push({ path: `/pages/mine/info?type=${index}` });
  205. }
  206. function handleChange(value: any) {
  207. PersonalInformation.value = { ...PersonalInformation.value, [value.key]: value.value };
  208. }
  209. const containsChinese = (str: string) => /[\u4E00-\u9FA5]/.test(str);
  210. const convertToPinyin = (value: string) =>
  211. containsChinese(value)
  212. ? pinyin(value, { toneType: "none", type: "capitalize" })
  213. : value;
  214. async function getUserSingle() {
  215. if (!userToken.value) {
  216. uni.showToast({ title: t("common.loginFirst"), icon: 'none' });
  217. return;
  218. }
  219. if (current.value != 1) {
  220. return;
  221. }
  222. try {
  223. const res = await personalApi.CustomLoginInfo();
  224. if (res.code === 200 && res.data) {
  225. // 更新表单数据
  226. PersonalInformation.value = {
  227. ...res.data.customInfo,
  228. customType: res.data.customInfo?.customType || 1,
  229. addressLines1: res.data.customInfo?.addressLines[0] || '',
  230. addressLines2: res.data.customInfo?.addressLines[1] || '',
  231. };
  232. userStore.saveUserInfo(res.data);
  233. // 设置状态
  234. if (res.data) {
  235. isUpdate.value = true;
  236. }
  237. if (res.data.approveStatus == 2 || res.data.kycStatus == 2) {
  238. isAuthInfo.value = true;
  239. }
  240. if (res.data.approveStatus == 3) {
  241. isAuthInfo.value = false;
  242. }
  243. }
  244. getCountry()
  245. } catch (error: any) {
  246. console.error('Error in getUserSingle:', error);
  247. uni.showToast({ title: error.message || t("common.error"), icon: 'none' });
  248. }
  249. }
  250. async function infoSubmit() {
  251. try {
  252. const res = await ucardApi.merchantRegister(PersonalInformation.value);
  253. if (res.code === 200) {
  254. uni.showToast({ title: t("common.success"), icon: 'success' });
  255. // 提交成功后打开 WebSDK 弹窗
  256. router.push({
  257. path: "/pages/mine/kyc",
  258. query: { cardId: (PersonalInformation.value as any).id },
  259. });
  260. } else {
  261. uni.showToast({ title: res.msg, icon: 'none' });
  262. }
  263. } catch (error: any) {
  264. uni.showToast({ title: error.message || t("common.error"), icon: 'none' });
  265. }
  266. }
  267. const isReadonly = computed(() => isAuthInfo.value);
  268. onMounted(async () => {
  269. // 先加载选项数据
  270. await Promise.all([
  271. getUserSingle(),
  272. ]);
  273. });
  274. </script>
  275. <style scoped lang="scss">
  276. @import "@/uni.scss";
  277. .tab-title {
  278. display: flex;
  279. align-items: center;
  280. gap: px2rpx(4);
  281. border: 1px solid #f3f4f6;
  282. font-size: px2rpx(18);
  283. padding: px2rpx(8) px2rpx(16);
  284. border-radius: px2rpx(2);
  285. background-color: white;
  286. }
  287. .active {
  288. background-color: var(--color-error);
  289. color: #fff;
  290. border: none;
  291. }
  292. .info-card {
  293. .btns {
  294. display: flex;
  295. justify-content: flex-end;
  296. gap: px2rpx(30);
  297. margin-top: px2rpx(30);
  298. .btn-primary {
  299. min-width: px2rpx(120);
  300. background-color: var(--color-navy-900);
  301. color: white;
  302. padding: 0 px2rpx(12);
  303. border-radius: px2rpx(8);
  304. border: none;
  305. font-size: px2rpx(14);
  306. text-align: center;
  307. cursor: pointer;
  308. display: flex;
  309. align-items: center;
  310. justify-content: center;
  311. gap: px2rpx(8);
  312. }
  313. .btn-primary:active {
  314. background-color: var(--color-navy-700);
  315. }
  316. }
  317. }
  318. .form-tab {
  319. height: px2rpx(100);
  320. }
  321. .form-section {
  322. margin: px2rpx(8) 0;
  323. }
  324. :deep(.u-uploader) {
  325. width: 100%;
  326. .u-uploader__wrapper {
  327. width: 100%;
  328. display: block;
  329. }
  330. .u-uploader__preview {
  331. width: 100% !important;
  332. height: px2rpx(160) !important;
  333. border-radius: px2rpx(24);
  334. overflow: hidden;
  335. .u-uploader__preview-image {
  336. width: 100%;
  337. height: 100%;
  338. .u-image__img {
  339. object-fit: contain;
  340. }
  341. }
  342. .u-uploader__preview-delete {
  343. position: absolute;
  344. top: 0;
  345. right: 0;
  346. width: px2rpx(30);
  347. height: px2rpx(30);
  348. border-radius: 0 px2rpx(24) 0 0;
  349. i {
  350. text-align: center;
  351. line-height: px2rpx(30);
  352. font-size: px2rpx(30);
  353. }
  354. }
  355. }
  356. }
  357. </style>