Просмотр исходного кода

提交

Signed-off-by: zhb <zhb@7stars.com>
zhb 10 месяцев назад
Родитель
Сommit
76c8f23311

+ 0 - 2
src/App.vue

@@ -79,8 +79,6 @@ const headerTitle = computed(() => {
     return t(`${key}.title`) || ''
 })
 onMounted(() => {
-    console.log(theme, 12)
-
     document.documentElement.classList.toggle('dark', theme.value === 'dark')
 })
 </script>

+ 1 - 1
src/api/ucard.ts

@@ -285,7 +285,7 @@ export const ucardApi = {
         return $api.post("/ucard/api/transfer/banks", { ...params });
     },
     // 查询法币汇率
-    ucardRate(params: { fromCurrency: string; toCurrency: string }): Promise<BaseResponse<RateInfo>> {
+    ucardRate(params: { targetCurrency: string; currency: string; targetCountry:string }): Promise<BaseResponse<RateInfo>> {
         return $api.post("/ucard/api/transfer/rate", { ...params });
     },
     // 代付校验

+ 3 - 0
src/assets/icons/copy.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="8" height="10" viewBox="0 0 8 10" fill="none">
+  <path d="M7.12448 7.78934H2.86712C2.65425 7.78934 2.47202 7.71355 2.32043 7.56196C2.16885 7.41037 2.09305 7.22815 2.09305 7.01528V1.59682C2.09305 1.38395 2.16885 1.20172 2.32043 1.05014C2.47202 0.898548 2.65425 0.822754 2.86712 0.822754H5.25705C5.36025 0.822754 5.45862 0.842106 5.55216 0.880809C5.64569 0.919512 5.72793 0.974342 5.79889 1.0453L7.676 2.92241C7.74696 2.99336 7.80179 3.07561 7.84049 3.16914C7.87919 3.26267 7.89854 3.36104 7.89854 3.46425V7.01528C7.89854 7.22815 7.82275 7.41037 7.67116 7.56196C7.51957 7.71355 7.33735 7.78934 7.12448 7.78934ZM7.12448 3.53198H5.76986C5.6086 3.53198 5.47153 3.47554 5.35864 3.36266C5.24576 3.24977 5.18931 3.1127 5.18931 2.95143V1.59682H2.86712V7.01528H7.12448V3.53198ZM1.31899 9.33747C1.10612 9.33747 0.923891 9.26168 0.772304 9.11009C0.620716 8.9585 0.544922 8.77628 0.544922 8.56341V3.53198C0.544922 3.42232 0.582013 3.3304 0.656194 3.25622C0.730375 3.18204 0.822295 3.14495 0.931955 3.14495C1.04161 3.14495 1.13353 3.18204 1.20772 3.25622C1.2819 3.3304 1.31899 3.42232 1.31899 3.53198V8.56341H5.18931C5.29897 8.56341 5.39089 8.6005 5.46508 8.67468C5.53926 8.74886 5.57635 8.84078 5.57635 8.95044C5.57635 9.0601 5.53926 9.15202 5.46508 9.2262C5.39089 9.30038 5.29897 9.33747 5.18931 9.33747H1.31899Z" fill="#595959"/>
+</svg>

BIN
src/assets/images/c-type.png


BIN
src/assets/images/error.png


BIN
src/assets/images/logo1.png


BIN
src/assets/images/success.png


BIN
src/assets/images/visa1.png


+ 30 - 1
src/assets/scss/global/global.scss

@@ -556,4 +556,33 @@ body {
     overflow: hidden;
     text-overflow: ellipsis;
     width: 100%;
-}
+}
+
+.custom-toast{
+    background: #fff;
+display: flex;
+width: 348px;
+padding: 32px 24px;
+flex-direction: column;
+align-items: center;
+gap: 16px;
+border-radius: 20px;
+background: #FFF;
+top: 40%;
+box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
+
+    .van-icon__image{
+        width: 88px;
+        height: 88px;
+    }
+    .van-toast__text {
+        color: #474747;
+        text-align: center;
+        font-family: Roboto;
+        font-size: 14px;
+        font-style: normal;
+        font-weight: 400;
+        line-height: 20px;
+        letter-spacing: 0.07px;
+    }
+}

+ 2 - 5
src/components.d.ts

@@ -16,6 +16,8 @@ declare module 'vue' {
     FirstApply: typeof import('./components/FirstApply.vue')['default']
     Icon: typeof import('./components/Icon.vue')['default']
     JsxComponents: typeof import('./components/jsx-components.tsx')['default']
+    Kyc: typeof import('./components/kyc.vue')['default']
+    KycInfo: typeof import('./components/KycInfo.vue')['default']
     Logout: typeof import('./components/Logout.vue')['default']
     MoreSelect: typeof import('./components/MoreSelect.vue')['default']
     PageHeader: typeof import('./components/PageHeader.vue')['default']
@@ -30,15 +32,10 @@ declare module 'vue' {
     VanCalendar: typeof import('vant/es')['Calendar']
     VanCheckbox: typeof import('vant/es')['Checkbox']
     VanDialog: typeof import('vant/es')['Dialog']
-    VanDropdownItem: typeof import('vant/es')['DropdownItem']
-    VanDropdownMenu: typeof import('vant/es')['DropdownMenu']
     VanField: typeof import('vant/es')['Field']
-    'VanField=': typeof import('vant/es')['Field=']
     VanForm: typeof import('vant/es')['Form']
     VanIcon: typeof import('vant/es')['Icon']
     VanLoading: typeof import('vant/es')['Loading']
-    VanPicker: typeof import('vant/es')['Picker']
-    VanPopup: typeof import('vant/es')['Popup']
     VanSwipe: typeof import('vant/es')['Swipe']
     VanSwipeItem: typeof import('vant/es')['SwipeItem']
     VanUploader: typeof import('vant/es')['Uploader']

+ 1 - 3
src/components/ApplyRecord.vue

@@ -40,7 +40,7 @@
                     <div class="label">{{ t('apply-record-detail.p2') }}</div>
                     <div :class="statusClass(cardInfo.kycStatus)">{{ statusMap[cardInfo.kycStatus] }}</div>
                 </div>
-                <div class="g-item" v-if="cardInfo.applyStatus === 0 || cardInfo.applyStatus === 1">
+                <div class="g-item" v-if="cardInfo.kycStatus == 2 &&(cardInfo.applyStatus === 0 || cardInfo.applyStatus === 1)">
                     <div class="label">{{ t('apply-record-detail.p7') }}</div>
                     <div :class="statusClass1(cardInfo.applyStatus)">{{ applyStatusMap[cardInfo.applyStatus] }}</div>
                 </div>
@@ -124,7 +124,6 @@ async function getKycList() {
         filtered = cardList.value.filter((item) => item.cardTypeId == props.cardTypeId)
     }
     cardInfo.value = filtered[0]
-    console.log(cardInfo.value, 12)
 }
 const statusMap: Record<string, string> = {
     null: t('kyc.statusDesc'),
@@ -676,7 +675,6 @@ onMounted(async () => {
 }
 
 .label {
-    width: 200px;
     font-weight: 600;
     font-size: 14px;
     line-height: 20px;

+ 435 - 0
src/components/KycInfo.vue

@@ -0,0 +1,435 @@
+<template>
+    <div class="page page-shadow">
+        <van-form ref="formRef" class="kyc-form" v-if="isShow && countryOptions.length > 0">
+            <remit-input
+                v-model:value="infoForm.lastName"
+                fkey="lastName"
+                :required="true"
+                :label="t('improve-info.p8')"
+                :rules="rules['lastName']"
+                @change="handleChange"
+            />
+            <remit-input
+                v-model:value="infoForm.firstName"
+                fkey="firstName"
+                :required="true"
+                :label="t('improve-info.p9')"
+                :rules="rules['firstName']"
+                @change="handleChange"
+            />
+
+            <remit-input
+                v-model:value="infoForm.email"
+                fkey="email"
+                :label="t('improve-info.p7')"
+                :required="true"
+                :rules="rules['email']"
+                @change="handleChange"
+            />
+            <div class="f">
+                <remit-input
+                    class="l"
+                    v-model:value="infoForm.areaCode"
+                    v-if="phoneCodes.length > 0"
+                    fkey="areaCode"
+                    :required="true"
+                    type="select"
+                    :show-search="true"
+                    :columns="phoneCodes"
+                    :label="t('improve-info.p6')"
+                    :rules="rules['areaCode']"
+                    @change="handleChange"
+                />
+                <remit-input class="r" v-model:value="infoForm.mobile" fkey="mobile" label=" " :rules="rules['mobile']" @change="handleChange" />
+            </div>
+            <remit-input
+                v-model:value="infoForm.sex"
+                fkey="sex"
+                type="select"
+                :columns="sexOptions"
+                :label="t('improve-info.p13')"
+                @change="handleChange"
+            />
+            <remit-input v-model:value="infoForm.birthday" type="date" fkey="birthday" :label="t('improve-info.p10')" @change="handleChange" />
+            <remit-input
+                v-model:value="infoForm.nationality"
+                fkey="nationality"
+                type="select"
+                :show-search="true"
+                :columns="countryOptions"
+                :label="t('improve-info.p17')"
+                @change="handleChange"
+            />
+            <remit-input
+                v-model:value="infoForm.town"
+                fkey="town"
+                :show-search="true"
+                type="select"
+                :columns="cityOptions"
+                :label="t('improve-info.p19')"
+                @change="handleChange"
+            />
+            <remit-input
+                v-model:value="infoForm.address"
+                fkey="address"
+                :label="t('improve-info.p21')"
+                :required="true"
+                :rules="rules['address']"
+                @change="handleChange"
+            />
+            <remit-input v-model:value="infoForm.postCode" fkey="postCode" :label="t('improve-info.p22')" @change="handleChange" />
+            <remit-input
+                v-model:value="infoForm.idType"
+                fkey="idType"
+                type="select"
+                :columns="idTypeOptions"
+                :label="t('improve-info.p23')"
+                @change="handleChange"
+            />
+            <remit-input v-model:value="infoForm.idNo" fkey="idNo" :label="t('improve-info.p27')" @change="handleChange" />
+            <remit-input
+                v-model:value="infoForm.idNoExpiryDate"
+                type="date"
+                fkey="idNoExpiryDate"
+                :label="t('improve-info.p28')"
+                @change="handleChange"
+            />
+            <remit-input
+                v-model:value="infoForm.idPicture"
+                type="upload"
+                fkey="idPicture"
+                :label="t('improve-info.p29')"
+                :isUploadD="true"
+                accept="image/png, image/jpeg, image/jpg"
+                @change="handleChange"
+            >
+                <div class="cwg-upload">
+                    <van-icon name="back-top" />
+                    <p class="name">{{ t('improve-info.p28_1') }}{{ t('improve-info.p29') }}</p>
+                    <p class="back">{{ t('improve-info.p28_2') }}</p>
+                </div>
+            </remit-input>
+            <remit-input
+                v-model:value="infoForm.facePicture"
+                type="upload"
+                fkey="facePicture"
+                :label="t('improve-info.p30')"
+                :isUploadD="true"
+                accept="image/png, image/jpeg, image/jpg"
+                @change="handleChange"
+            >
+                <div class="cwg-upload">
+                    <van-icon name="back-top" />
+                    <p class="name">{{ t('improve-info.p28_1') }}{{ t('improve-info.p30') }}</p>
+                    <p class="back">{{ t('improve-info.p28_2') }}</p>
+                </div>
+            </remit-input>
+            <div class="fixed-btn">
+                <div class="cwg-button">
+                    <van-button type="primary" block @click="infoSubmit">{{ t('improve-info.p33') }}</van-button>
+                </div>
+            </div>
+        </van-form>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { pinyin } from 'pinyin-pro'
+import { useRouter } from 'vue-router'
+import { useI18n } from 'vue-i18n'
+import { showToast } from 'vant'
+import { ucardApi } from '@/api/ucard'
+import { userApi } from '@/api/user'
+import useUserStore from '@/stores/use-user-store'
+const userStore = useUserStore()
+const userInfo = computed(() => userStore.userInfo)
+const { t } = useI18n()
+const router = useRouter()
+const route = useRoute()
+const formRef = ref()
+const { type,cardTypeId  } = route.query as { type: string,cardTypeId:string }
+const sexOptions = ref([
+    { text: t('improve-info.p15'), value: '1' },
+    { text: t('improve-info.p16'), value: '2' },
+])
+const idTypeOptions = ref([
+    { text: t('improve-info.p25'), value: 'EUROPEAN_ID' },
+    { text: t('improve-info.p26'), value: 'PASSPORT' },
+])
+// 表单验证规则
+const rules = {
+    email: [{ required: true, message: t('improve-info.p3_3_5') }],
+    lastName: [{ required: true, message: t('improve-info.p3_1') }],
+    firstName: [{ required: true, message: t('improve-info.p3_2') }],
+    address: [{ required: true, message: t('improve-info.p3_4') }],
+    mobile: [{ required: true, message: t('improve-info.p3_3') }],
+    areaCode: [{ required: true, message: t('improve-info.p5') }],
+}
+const infoForm = ref({
+    // payerType: 'INDIVIDUAL',
+    // payerLastName: 'zhang',
+    // payerFirstName: 'tom',
+    // payerIdNo: 'ES125664',
+    // payerIdNoType: 'PASSPORT',
+    // payerIdCountry: 'SG',
+    // payerBirthday: '1990-10-10',
+    // payerNationalityCountry: 'SG',
+    // payerMobile: '+65012345678',
+    // payerCountryCode: 'SG',
+    // payerCityCode: 'SG_1',
+    // payerAddress: '21 Tampines Ave 1, Singapore 529757',
+    // payerPostCode: '999002',
+    // payerOccupation: 'worker',
+
+    areaCode: undefined,
+    country: undefined,
+    mobile: undefined,
+    idNo: undefined,
+    sex: undefined,
+    birthday: undefined,
+    nationality: undefined,
+    town: undefined,
+    postCode: undefined,
+    idType: undefined,
+    idNoExpiryDate: undefined,
+    idPicture: undefined,
+    facePicture: undefined,
+    email: undefined,
+    lastName: undefined,
+    firstName: undefined,
+    address: undefined,
+    cId: undefined,
+    cardTypeId: undefined,
+})
+const formData = ref<typeof infoForm.value>({} as any)
+const formDatas = ref<typeof infoForm.value>({} as any)
+const isShow = ref(false)
+
+// 国家选项
+const countryOptions = ref<Array<{ text: string; value: string }>>([])
+const cityOptions = ref<Array<{ text: string; value: string }>>([])
+const phoneCodes = ref([])
+// 获取国家列表
+const getCountryListForSelect = async () => {
+    try {
+        const res = await ucardApi.ucardCountryCity({})
+        if (res.code === 200 || res.code === 0) {
+            countryOptions.value = res.data.map((item: any) => ({
+                text: lang.value === 'cn' ? item.cnName : item.enName,
+                value: item.code,
+            }))
+        }
+    } catch (error) {
+        countryOptions.value = []
+    }
+}
+// 获取城市列表
+const getCityListForSelect = async (countryCode: string) => {
+    try {
+        const res = await ucardApi.ucardCountryCity({ code: countryCode })
+        if (res.code === 200 || res.code === 0) {
+            const cityList = res.data.map((item: any) => ({
+                text: lang.value === 'cn' ? item.cnName : item.enName,
+                value: item.code,
+            }))
+            cityOptions.value = cityList
+        }
+    } catch (error) {
+        cityOptions.value = []
+    }
+}
+const getCountry = async () => {
+    try {
+        const res = await ucardApi.countryGet()
+        phoneCodes.value = res.data.map((item: { callingCode: string; name: string; enName: string }) => ({
+            text: lang.value === 'cn' ? item.name + ' + ' + item.callingCode : item.enName + ' + ' + item.callingCode,
+            value: item.callingCode,
+        }))
+    } catch (error) {
+        showToast(t('common.error'))
+    }
+}
+const infoSubmit = async () => {
+    try {
+        const requiredFields = ['email', 'lastName', 'firstName', 'address', 'mobile', 'areaCode'] as const
+        await formRef.value?.validate(requiredFields)
+    } catch (error) {
+        if (Array.isArray(error) && error.length > 0) {
+            showToast(error[0].message)
+        } else {
+            showToast(t('improve-info.errer'))
+        }
+        return
+    }
+
+    let res
+    if (formData.value.uniqueId) {
+        res = await ucardApi.merchantUpdate(formData.value as any)
+        if (res.code === 200) {
+            switch (type) {
+                case '1':
+                    showToast(t('improve-info.kycSuccess'))
+                    router.push('/mine')
+                    break
+                case '3':
+                kycSubmit()
+                    break
+                default:
+                    break
+            }
+        }
+    } else {
+        res = await ucardApi.merchantRegister(formData.value as any)
+        if (res.code === 200) {
+            showToast(t('improve-info.kycSuccess'))
+            userInfo.value.customInfo.uniqueId = res.data
+            userStore.saveUserInfo(userInfo.value)
+            router.push('/')
+        }
+    }
+}
+const handleChange = (value: any) => {
+    formData.value = { ...formData.value, [value.key]: value.value }
+    if (value.key === 'nationality') {
+        formData.value = { ...formData.value, country: value.value }
+        if(userInfo.value?.customInfo.nationality == value.value) return
+        getCityListForSelect(value.value)
+        infoForm.value.town = ''
+    }
+}
+const containsChinese = (str: string) => /[\u4e00-\u9fa5]/.test(str)
+const convertToPinyin = (value: string) => (containsChinese(value) ? pinyin(value, { toneType: 'none', type: 'capitalize' }) : value)
+const getInfoForm = () => {
+    const customInfo = userInfo.value?.customInfo || {}
+    const fields = [
+        'areaCode',
+        'mobile',
+        'idNo',
+        'sex',
+        'birthday',
+        'nationality',
+        'town',
+        'postCode',
+        'idType',
+        'idNoExpiryDate',
+        'idPicture',
+        'facePicture',
+        'email',
+        'lastName',
+        'firstName',
+        'address',
+        'cId',
+        'uniqueId',
+    ]
+
+    const data: Record<string, any> = {}
+    for (const key of fields) {
+        data[key] = customInfo[key] ?? undefined
+    }
+    if (customInfo.uniqueId) {
+        for (const key of fields) {
+            data[key] = formDatas.value[key] ?? undefined
+        }
+    }
+    data.lastName = convertToPinyin(data.lastName)
+    data.firstName = convertToPinyin(data.firstName)
+    infoForm.value = { ...data }
+    formData.value = { ...formData.value, ...data, cardTypeId }
+}
+const getUserInfo = async () => {
+    if (!userToken.value) {
+        showToast('请先登录')
+        return
+    }
+    try {
+        const res = await userApi.getUserSingle()
+        if (res.code === 200 && res.data.uniqueId) {
+            formDatas.value = res.data
+            getInfoForm()
+        }
+    } catch (error: any) { }
+}
+
+const kycSubmit = async () => {
+    const {uniqueId,cardTypeId} = formData.value
+    const res = await ucardApi.kycSubmit({uniqueId,cardTypeId})
+    if (res.code === 200) {
+        showToast(t('kyc.kycSuccess'))
+        router.push('/cards')
+    }
+}
+onMounted(async () => {
+    if (type == '1' || type == '3') {
+        getUserInfo()
+    } else {
+        getInfoForm()
+    }
+    isShow.value = false
+
+    await getCountry()
+    await getCountryListForSelect()
+    if (infoForm.value.nationality) {
+        await getCityListForSelect(infoForm.value.nationality)
+        isShow.value = true
+    } else {
+        isShow.value = true
+    }
+})
+</script>
+
+<style scoped lang="scss">
+.f {
+    display: flex;
+    align-items: flex-end;
+    gap: 12px;
+
+    .l {
+        flex: 1;
+    }
+
+    .r {
+        width: 273px;
+    }
+}
+
+:deep(.van-uploader) {
+    width: 100%;
+
+    .van-uploader__wrapper {
+        width: 100%;
+        display: block;
+    }
+}
+
+::v-deep(.van-uploader__preview) {
+    width: 100% !important;
+    height: 160px !important;
+    border: 1px dashed #beb6b6;
+    border-radius: 24px;
+    overflow: hidden;
+
+    .van-uploader__preview-image {
+        width: 100%;
+        height: 100%;
+
+        .van-image__img {
+            object-fit: contain;
+        }
+    }
+
+    .van-uploader__preview-delete {
+        position: absolute;
+        top: 0;
+        right: 0;
+        width: 30px;
+        height: 30px;
+        border-radius: 0 24px 0 0;
+
+        i {
+            text-align: center;
+            line-height: 30px;
+            font-size: 30px;
+        }
+    }
+}
+</style>

+ 3 - 1
src/components/MoreSelect.vue

@@ -7,7 +7,7 @@
                         class="form-input"
                         v-model="inputValueDoc"
                         type="text"
-                        placeholder="Type a currency / country"
+                        :placeholder="t('common.input')"
                         :clearable="true"
                         autocomplete="off"
                     >
@@ -34,6 +34,8 @@
 </template>
 
 <script setup lang="ts">
+import { useI18n } from 'vue-i18n'
+const { t } = useI18n()
 const props = defineProps<{
     modelValue: boolean
     showSearch: Boolean

+ 25 - 5
src/components/RemitInput.vue

@@ -38,6 +38,7 @@
                 autocomplete="off"
                 :rules="[...rules]"
                 :maxlength="maxlength"
+                :max="max"
                 :error-message="errorMessage"
                 @blur="handleBlur"
                 @focus="handleFocus"
@@ -112,8 +113,15 @@
         <template v-if="type === 'upload'">
             <van-field name="uploader" class="form-input uploader" :rules="[...rules]">
                 <template #input>
-                    <van-uploader :accept="accept" v-model="uploader" :max-count="1" :after-read="afterRead" v-if="!isUploadD"></van-uploader>
-                    <van-uploader v-model="uploader" :max-count="1" :after-read="afterRead" v-if="isUploadD">
+                    <van-uploader
+                        :disabled="disabled"
+                        :accept="accept"
+                        v-model="uploader"
+                        :max-count="1"
+                        :after-read="afterRead"
+                        v-if="!isUploadD"
+                    ></van-uploader>
+                    <van-uploader :disabled="disabled" v-model="uploader" :max-count="1" :after-read="afterRead" v-if="isUploadD">
                         <slot></slot>
                     </van-uploader>
                 </template>
@@ -162,6 +170,7 @@ const props = defineProps({
     disabled: Boolean,
     readonly: Boolean,
     required: Boolean,
+    max: Number,
     clearable: { type: Boolean, default: true },
     columns: { type: Array, default: () => [] },
     rules: { type: Array, default: () => [] },
@@ -191,7 +200,7 @@ const filteredColumns = computed(() => {
 watch(
     () => inputValueDoc.value,
     (newVal) => {
-        if (!newVal) return
+        // if (!newVal) return
         if (props.type === 'text' || props.type === 'number' || props.type === 'password') {
             emit('update:value', newVal)
             emit('change', { value: newVal, key: props.fkey })
@@ -221,7 +230,12 @@ watch(
             inputValueDoc.value = newVal ? dayjs(newVal).format('YYYY-MM-DD') : ''
         } else if (props.type === 'select') {
             const matched = props.columns.find((opt) => opt.value === newVal)
-            inputValueDoc.value = matched?.text || ''
+            if (props.fkey == 'areaCode') {
+                inputValueDoc.value = matched?.text.match(/\+ \d+$/)[0]
+            } else {
+                inputValueDoc.value = matched?.text || ''
+            }
+
             selectedValue.value = [matched?.value]
         } else if (props.type === 'upload') {
             uploader.value = [{ url: props.value }]
@@ -252,7 +266,12 @@ const handleClear = () => {
 }
 
 const onConfirm = (value) => {
-    const selectedText = value.text || ''
+    let selectedText = value.text || ''
+    console.log(props.fkey, value.text, 11)
+    if (props.fkey == 'areaCode') {
+        selectedText = selectedText.match(/\+ \d+$/)[0]
+    }
+
     inputValueDoc.value = selectedText
     showPicker.value = false
 }
@@ -289,6 +308,7 @@ const onDateConfirm = (value) => {
     border-radius: 10px !important;
     font-size: 16px !important;
     margin-top: 4px;
+    padding: 12px;
 
     ::v-deep .van-field__control {
         &::placeholder {

+ 414 - 78
src/components/VirtualCard.vue

@@ -9,23 +9,36 @@
                     :style="{ transform: `translateX(${(index - currentIndex) * 100 + offsetX}%)` }"
                 >
                     <div class="card-info" @click.stop="(e: MouseEvent) => toggleCardNo(card.id, e)" :class="{ flipping: isFlipping[card.id] }">
-                        <img src="@/assets/images/logo.png" alt="" srcset="" />
-                        <div class="number" v-if="showCardNo[card.id]">
-                            {{ card?.cardNo}}
-                        </div>
-                        <div class="number" v-else>
-                            {{ card?.cardNo?.replace(/(\d{4})\d+(\d{4})/, '$1 **** **** $2') }}
-                        </div>
-                        <div class="card-b">
-                            <div class="valid">
-                                <span class="lable">{{ t('cards.p13') }}</span>
-                                <span>{{ card.firstName }} {{ card.lastName }}</span>
+                        <p class="card-name">Virtual</p>
+                        <img class="card-type" src="@/assets/images/c-type.png" alt="" srcset="" />
+                        <img class="logo" src="@/assets/images/logo1.png" alt="" srcset="" />
+                        <template v-if="showCardNo[card.id]">
+                            <div class="number">
+                                {{ card?.cardNo }}
+                                <span class="copy" @click.stop="cardCopy(card.cardNo, 'cardNo')"><icon name="copy" :size="23" color="" /></span>
                             </div>
-                            <div class="valid">
-                                <span class="lable">{{ t('cards.p14') }}</span>
-                                <span>{{ card.expire }}</span>
+                            <div class="card-b">
+                                <div class="valid">
+                                    <span class="lable">{{ t('cards.p13') }}</span>
+                                    <span>{{ card.firstName }} {{ card.lastName }}</span>
+                                </div>
+                                <div class="valid">
+                                    <span class="lable">{{ t('cards.p14') }}</span>
+                                    <span>{{ card.expire }}</span>
+                                </div>
+                                <div class="valid">
+                                    <span class="lable">CVV</span>
+                                    <div class="cvv">
+                                        {{ card.cvv }}
+                                        <span class="copy" @click.stop="cardCopy(card.cvv, 'cvv')"><icon name="copy" :size="23" color="" /></span>
+                                    </div>
+                                </div>
                             </div>
-                        </div>
+                        </template>
+                        <template v-else>
+                            <div class="zw">**** 7549</div>
+                        </template>
+
                         <img
                             v-if="card.status != 1"
                             src="https://upload.wikimedia.org/wikipedia/commons/a/a4/Flag_of_the_United_States.svg"
@@ -65,7 +78,7 @@
             </div>
         </div>
         <template v-if="!currentCard.isOk">
-            <div class="balance-content">{{ t('apply-record-detail.p1') }}</div>
+            <div class="balance-content1">{{ t('apply-record-detail.p1') }}</div>
             <div class="g">
                 <img src="../assets/images/apply-record-1.png" alt="" />
                 <div class="g-l">
@@ -76,7 +89,7 @@
                         <div class="label">{{ t('apply-record-detail.p2') }}</div>
                         <div :class="statusClass(currentCard.kycStatus)">{{ statusMap[currentCard.kycStatus] }}</div>
                     </div>
-                    <div class="g-item" v-if="currentCard.applyStatus === 0 || currentCard.applyStatus === 1">
+                    <div class="g-item" v-if="currentCard.kycStatus == 2 && (currentCard.applyStatus === 0 || currentCard.applyStatus === 1)">
                         <div class="label">{{ t('apply-record-detail.p7') }}</div>
                         <div :class="statusClass1(currentCard.applyStatus)">{{ applyStatusMap[currentCard.applyStatus] }}</div>
                     </div>
@@ -84,25 +97,31 @@
             </div>
 
             <div class="cwg-button cwg-btn">
-            <van-button type="primary" block v-if="currentCard.kycStatus === 2 && currentCard.applyStatus === null" @click="handleApply(2, currentCard)">{{
-                t('cards.p6')
-            }}</van-button>
-            <van-button type="primary" block v-if="[null, -1, 1, 3].includes(currentCard.kycStatus)" @click="handleApply(3, currentCard)">{{
-                t('cards.p7')
-            }}</van-button>
-        </div>
+                <van-button
+                    type="primary"
+                    block
+                    v-if="currentCard.kycStatus === 2 && currentCard.applyStatus === null"
+                    @click="handleApply(2, currentCard)"
+                    >{{ t('cards.p6') }}</van-button
+                >
+                <van-button type="primary" block v-if="[null, -1, 1, 3].includes(currentCard.kycStatus)" @click="handleApply(3, currentCard)">{{
+                    t('cards.p7')
+                }}</van-button>
+            </div>
         </template>
         <template v-if="currentCard.isOk">
             <div class="balance-wrap" v-if="balance.length > 0">
                 <div class="balance-content">{{ t('cards.currency') }}</div>
-                <div class="balance-title">{{ t('cards.balance') }}</div>
             </div>
-            <div class="balance-wrap" v-for="item in balance" :key="item.currency">
-                <div class="currency">
-                    <img :src="imageSrc(item.currency)" class="flag" />
-                    <span>{{ item.currency }}</span>
+            <div class="balance-wrap">
+                <div class="global-con-l" @click="setModelValue">
+                    <img :src="imageSrc(currency)" alt="" srcset="" />
+                    <div class="r">
+                        <p>{{ currency }}</p>
+                    </div>
+                    <icon name="icon_dropdown" :size="24" />
                 </div>
-                <div class="balance">{{ item.amount }}</div>
+                <div class="balance">{{ amount }}</div>
             </div>
             <div class="trans-header">
                 <div class="trans-title">{{ t('cards.transactions') }}</div>
@@ -122,25 +141,53 @@
                     @confirm="onConfirmStart"
                 />
             </div>
-            <div class="transactions" v-if="transactions.length > 0">
-                <div v-for="t in transactions" :key="t.id" class="transaction" @click="goToTransactionDetail(t)">
-                    <div class="trans-icon">
-                        <div class="trans-icon-inner">
-                            <i class="i-mdi-cart-outline"></i>
+
+            <div class="tab">
+                <div class="item" :class="{ active: jiluIndex === 0 }" @click="jiluIndex = 0">{{ t('cards.rechargeB1') }}</div>
+                <div class="item" :class="{ active: jiluIndex === 1 }" @click="jiluIndex = 1">{{ t('cards.transferB2') }}</div>
+            </div>
+            <template v-if="jiluIndex === 0">
+                <div class="recharge" v-if="recharge.length > 0">
+                    <div v-for="i in recharge" :key="i.id" class="transaction" @click="goToTransactionDetail1(i)">
+                        <div class="trans-icon">
+                            <div class="trans-icon-inner">$</div>
+                        </div>
+                        <div class="trans-left">
+                            <div class="trans-type">{{ i.typeDesc }}</div>
+                            <div class="trans-desc">{{ statusRechargeMap[i.status] }}</div>
+                        </div>
+                        <div class="trans-right">
+                            <div class="trans-amount">{{ i.amount }} {{ i.currency }}</div>
+                            <div class="trans-date">{{ i.time }}</div>
                         </div>
                     </div>
-                    <div class="trans-left">
-                        <div class="trans-type">{{ t.tradeTypeStr }}</div>
-                        <div class="trans-desc">{{ t.remark }}</div>
-                    </div>
-                    <div class="trans-right">
-                        <div class="trans-amount">{{ t.amount }} {{ t.currencyTxn }}</div>
-                        <div class="trans-date">{{ t.businessDate }}</div>
+                </div>
+                <div class="recharge" v-else>
+                    <EmptyState :title="t('empty-state.t2')" :text="t('empty-state.c2')" />
+                </div>
+            </template>
+            <template v-else>
+                <div class="transactions" v-if="transactions.length > 0">
+                    <div v-for="t in transactions" :key="t.id" class="transaction" @click="goToTransactionDetail(t)">
+                        <div class="trans-icon">
+                            <div class="trans-icon-inner">
+                                <i class="i-mdi-cart-outline"></i>
+                            </div>
+                        </div>
+                        <div class="trans-left">
+                            <div class="trans-type">{{ t.tradeTypeStr }}</div>
+                            <div class="trans-desc">{{ t.remark }}</div>
+                        </div>
+                        <div class="trans-right">
+                            <div class="trans-amount">{{ t.amount }} {{ t.currencyTxn }}</div>
+                            <div class="trans-date">{{ t.businessDate }}</div>
+                        </div>
                     </div>
                 </div>
-            </div>
-            <EmptyState :title="t('empty-state.t1')" :text="t('empty-state.c1')"
-        /></template>
+                <EmptyState v-if="transactions.length == 0" :title="t('empty-state.t1')" :text="t('empty-state.c1')" />
+            </template>
+        </template>
+        <CurrencySelect :showSearch="true" :options="balance" v-model="modelValue" @select="changeSelect" />
     </div>
 </template>
 
@@ -156,10 +203,16 @@ const router = useRouter()
 import useUserStore from '@/stores/use-user-store'
 const userStore = useUserStore()
 const cardList = computed(() => userStore.userCard)
+const userInfo = computed(() => userStore.userInfo)
 const { t } = useI18n()
 const currentCard = ref<CardInfo | null>({})
-const balance = ref<{ balance: number; currency: string }[]>([])
-const globalStore = useGlobalStore()
+const balance = ref<{ amount: number; currency: string; value: string }[]>([
+    {
+        amount: 0,
+        currency: 'USD',
+        value: 'USD',
+    },
+])
 const transactions = ref<TransactionInfo[]>([])
 const showCardNo = ref<{ [key: string]: boolean }>({})
 const isFlipping = ref<{ [key: string]: boolean }>({})
@@ -169,8 +222,71 @@ const offsetX = ref(0)
 const isDragging = ref(false)
 const images = import.meta.glob('@/assets/images/currency/*.png', { eager: true })
 
-const imageSrc = (currency:string) => {
-  return images[`/src/assets/images/currency/${currency}.png`]?.default || fallbackImg
+const imageSrc = (currency: string) => {
+    return images[`/src/assets/images/currency/${currency}.png`]?.default || fallbackImg
+}
+const jiluIndex = ref(0)
+
+import copyIcon from '@/assets/images/success.png'
+import errorIcon from '@/assets/images/error.png'
+const cardCopy = (content, type) => {
+    let message
+    if (type == 'cardNo') {
+        message = t('common.copy1')
+    } else if (type == 'cvv') {
+        message = t('common.copy2')
+    }
+    if (navigator.clipboard && window.isSecureContext) {
+        navigator.clipboard
+            .writeText(content)
+            .then(() => {
+                showToast({
+                    message,
+                    icon: copyIcon,
+                    className: 'custom-toast',
+                })
+            })
+            .catch((err) => {
+                showToast({
+                    message: t('common.copy3'),
+                    icon: errorIcon,
+                    className: 'custom-toast',
+                })
+            })
+    } else {
+        const textarea = document.createElement('textarea')
+        textarea.value = content
+        textarea.setAttribute('readonly', '')
+        textarea.style.position = 'absolute'
+        textarea.style.left = '-9999px'
+        document.body.appendChild(textarea)
+        textarea.select()
+        try {
+            document.execCommand('copy')
+            showToast({
+                message,
+                icon: copyIcon,
+                className: 'custom-toast',
+            })
+        } catch (err) {
+            showToast({
+                message: t('common.copy3'),
+                icon: errorIcon,
+                className: 'custom-toast',
+            })
+        }
+        document.body.removeChild(textarea)
+    }
+}
+const currency = ref('USD')
+const amount = ref(0)
+const modelValue = ref(false)
+const setModelValue = () => {
+    modelValue.value = true
+}
+const changeSelect = (e) => {
+    amount.value = e.amount
+    currency.value = e.currency
 }
 const showDatePicker = ref(false)
 const dateRange = ref<[string, string] | undefined>(undefined)
@@ -206,6 +322,40 @@ const ucardActivate = async (id: string) => {
     })
 }
 
+const recharge = ref<Transaction[]>([])
+
+// 获取充值记录
+async function getRechargeList() {
+    try {
+        recharge.value = []
+        if (!currentCard.value?.cardNo) return
+        const res = await ucardApi.rechargeList({
+            cardNo: currentCard.value?.cardNo,
+            page: { current: 1, row: 4 },
+        })
+        if (res.code === 200 && res.data) {
+            recharge.value = res.data
+        }
+    } catch (error) {
+        recharge.value = []
+        showToast(error as string)
+    }
+}
+const statusRechargeMap = {
+    '1': t('card-recharge.rechargeSuccess'),
+    '2': t('card-recharge.rechargeFailed'),
+    '3': t('card-recharge.rechargePending'),
+}
+
+const goToTransactionDetail1 = (i: TransactionInfo) => {
+    router.push({
+        path: '/recharge/record/detail',
+        query: {
+            orderNo: i.orderNo,
+        },
+    })
+}
+
 // 获取交易记录
 const getTransactions = async () => {
     try {
@@ -228,24 +378,55 @@ const getTransactions = async () => {
 }
 
 const getBalance = async () => {
-    globalStore.setFullScreenLoading(true)
     try {
-        balance.value = []
+        balance.value = [
+            {
+                amount: 0,
+                currency: 'USD',
+                value: 'USD',
+            },
+        ]
         if (!currentCard.value?.cardNo) return
         const res = await ucardApi.ucardBalance({
             cardNo: currentCard.value?.cardNo,
             uniqueId: currentCard.value?.uniqueId,
         })
+        res.data.map((item: any) => {
+            item.value = item.currency
+        })
+
         if (res.code == 200) {
+            res.data.push({
+                amount: 0,
+                currency: 'EUR',
+                value: 'EUR',
+            })
             balance.value = res.data
+            if (balance.value.length > 0) {
+                currency.value = balance.value[0].currency
+                amount.value = balance.value[0].amount
+            } else {
+                currency.value = 'USD'
+                amount.value = 0
+            }
         } else {
-            balance.value = []
+            balance.value = [
+                {
+                    amount: 0,
+                    currency: 'USD',
+                    value: 'USD',
+                },
+            ]
         }
     } catch (error: any) {
         showToast(error?.message || String(error))
-        balance.value = []
-    } finally {
-        globalStore.setFullScreenLoading(false)
+        balance.value = [
+            {
+                amount: 0,
+                currency: 'USD',
+                value: 'USD',
+            },
+        ]
     }
 }
 
@@ -299,12 +480,22 @@ const handleApply = (type: number, item: any) => {
             path: '/kyc',
             query: {
                 cardTypeId: item.cardTypeId,
-                type: 0,
+                type: 3,
             },
         })
     }
 }
 
+const ucardApply = async (item: any) => {
+    const res = await ucardApi.ucardApply({
+        cardTypeId: item.cardTypeId,
+        uniqueId: userInfo.value.customInfo.uniqueId,
+    })
+    if (res.code === 200) {
+        showToast(t('kyc.kycSuccess2'))
+    }
+}
+
 watch(
     currentIndex,
     (newIndex) => {
@@ -312,14 +503,30 @@ watch(
             currentCard.value = cardList.value[newIndex]
             if (cardList.value[newIndex].isOk) {
                 getTransactions()
+                getRechargeList()
                 getBalance()
             }
         }
     },
     { immediate: true },
 )
+watch(
+    jiluIndex,
+    (newIndex) => {
+        console.log('jiluIndex changed:', newIndex)
+        if (newIndex === 0) {
+            getRechargeList()
+        } else {
+            getTransactions()
+        }
+    },
+    // { immediate: true },
+)
 
 const toggleCardNo = (cardId: string, event: MouseEvent) => {
+    if (!cardId) {
+        return
+    }
     event.stopPropagation()
     isFlipping.value[cardId] = !isFlipping.value[cardId]
     showCardNo.value[cardId] = !showCardNo.value[cardId]
@@ -396,8 +603,8 @@ async function goToFreezeCard() {
             case 1:
                 let res = await ucardApi.ucardFreeze({ cardNo: currentCard.value?.cardNo, uniqueId: currentCard.value?.uniqueId })
                 if (res.code === 200) {
+                    currentCard.value.freezeStatus = 2
                     showToast('冻结成功')
-                    getCardList()
                 } else {
                     showToast(res.msg)
                 }
@@ -405,15 +612,15 @@ async function goToFreezeCard() {
             case 2:
                 let res1 = await ucardApi.ucardUnfreeze({ cardNo: currentCard.value?.cardNo, uniqueId: currentCard.value?.uniqueId })
                 if (res1.code === 200) {
+                    currentCard.value.freezeStatus = 1
                     showToast('解冻成功')
-                    getCardList()
                 } else {
                     showToast(res1.msg)
                 }
                 break
         }
     } catch (error) {
-        showToast(error || String(error))
+        showToast(t('common.error'))
     }
 }
 
@@ -435,7 +642,7 @@ onMounted(async () => {
 
 <style scoped lang="scss">
 .page {
-    padding: 0 16px;
+    padding: 0 16px 70px 16px;
 }
 .card-wrapper {
     position: absolute;
@@ -446,32 +653,66 @@ onMounted(async () => {
 }
 
 .card-info {
-    background: url(/src/assets/images/visa.png) no-repeat center center;
+    background: url(/src/assets/images/visa1.png) no-repeat center center;
     background-size: cover;
-    border-radius: 0.426667rem;
-    padding: 0.64rem 0.533333rem 0 0.533333rem;
+    border-radius: 16px;
+    padding: 16px;
     color: var(--main-yellow);
-    box-shadow: 0 0.106667rem 0.533333rem rgba(214, 255, 0, 0.1);
-    border: 1px solid rgba(214, 255, 0, 0.2);
     width: 100%;
     height: 100%;
     display: flex;
     justify-content: flex-start;
     align-items: baseline;
     flex-wrap: wrap;
-    transform-style: preserve-3d;
-    transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
-    flex-direction: column;
+    /* transform-style: preserve-3d;
+    transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1); */
+    /* flex-direction: column; */
+
+    .logo {
+        width: 120px;
+        height: auto;
+        margin-bottom: 16px;
+    }
+    .card-type {
+        width: 74px;
+        height: 39px;
+        position: absolute;
+        bottom: 16px;
+        right: 16px;
+        z-index: 1;
+    }
+    .card-name {
+        position: absolute;
+        top: 16px;
+        right: 16px;
+        font-size: var(--font-size-14);
+        font-weight: 500;
+        color: #fff;
+        font-family: Rubik;
+    }
 
     &.flipping {
         transform: rotateX(360deg);
     }
+    .zw {
+        position: absolute;
+        left: 16px;
+        bottom: 16px;
+        color: #fff;
+        font-family: Rubik;
+        font-size: 12px;
+        font-style: normal;
+        font-weight: 700;
+        display: flex;
+        align-items: center;
+    }
 
     .card-b {
         display: flex;
         flex-wrap: wrap;
         justify-content: center;
         align-items: center;
+        gap: 16px;
         span {
             display: block;
         }
@@ -479,15 +720,37 @@ onMounted(async () => {
             font-size: var(--font-size-14);
             font-weight: 500;
             color: var(--black);
-            text-shadow: 0 0 8px rgba(214, 255, 0, 0.2);
             gap: 8px;
-            padding-right: 30px;
             line-height: 20px;
         }
         .lable {
             font-size: var(--font-size-10);
             font-weight: 400;
         }
+        .cvv {
+            display: flex;
+            align-items: center;
+            gap: 8px;
+            font-size: var(--font-size-14);
+            font-weight: 500;
+            color: var(--black);
+            .copy,
+            .icon {
+                display: flex;
+            }
+        }
+    }
+
+    .copy {
+        width: 18px;
+        height: 18px;
+        border-radius: 50%;
+        background: rgba(255, 255, 255, 0.6);
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        cursor: pointer;
+        color: #000;
     }
 }
 
@@ -511,7 +774,6 @@ onMounted(async () => {
     font-size: var(--font-size-14);
     line-height: 2;
     margin-bottom: 8px;
-    text-shadow: 0 0 10px rgba(214, 255, 0, 0.3);
     display: flex;
     align-items: center;
     gap: 8px;
@@ -528,8 +790,7 @@ onMounted(async () => {
     font-weight: 500;
     line-height: 3;
     letter-spacing: 2px;
-    margin: 24px 0;
-    text-shadow: 0 0 15px rgba(214, 255, 0, 0.4);
+    margin: 20px 0;
     display: flex;
     align-items: center;
     gap: 8px;
@@ -611,7 +872,35 @@ onMounted(async () => {
 .transactions {
     border-radius: 16px;
     margin-bottom: 16px;
-    padding: 16px 0;
+    padding-bottom: 16px;
+}
+.tab {
+    display: flex;
+    width: 380px;
+    align-items: center;
+    gap: 16px;
+    .item {
+        display: flex;
+        height: 36px;
+        padding: 8px 16px;
+        justify-content: center;
+        align-items: center;
+        gap: 10px;
+        border-radius: 50px;
+        border: 1px solid #beb6b6;
+        background: #fff;
+        color: #1a1a1a;
+        font-family: Roboto;
+        font-size: 16px;
+        font-style: normal;
+        font-weight: 600;
+        line-height: 24px;
+    }
+    .active {
+        background: #ff4766;
+        color: #fff;
+        border: none;
+    }
 }
 .trans-icon {
     width: 48px;
@@ -768,16 +1057,21 @@ onMounted(async () => {
 }
 
 .card-swiper {
+    width: 90%;
+    margin: 20px 5% 0 5%;
     position: relative;
     overflow: hidden;
-    margin-bottom: 20px;
+    display: flex;
+    justify-content: center;
+    flex-direction: column;
+    align-items: center;
     padding-bottom: 5px;
 }
 
 .swiper-container {
     position: relative;
     width: 100%;
-    height: 209px;
+    height: 219px;
     touch-action: pan-y pinch-zoom;
     user-select: none;
 }
@@ -794,7 +1088,7 @@ onMounted(async () => {
     display: flex;
     align-items: center;
     justify-content: center;
-    margin-top: 16px;
+    margin-top: 12px;
     gap: 16px;
 }
 
@@ -868,7 +1162,8 @@ onMounted(async () => {
     right: 10px;
 }
 
-.balance-content {
+.balance-content1 {
+    margin-bottom: 20px;
     font-size: var(--font-size-20);
     color: var(--white);
     font-weight: bold;
@@ -876,5 +1171,46 @@ onMounted(async () => {
 .cwg-btn {
     margin-top: 36px;
 }
+.global-con-l {
+    display: flex;
+    width: 164px;
+    padding: 12px 16px;
+    align-items: center;
+    gap: 12px;
+    border-radius: 12px;
+    box-shadow: 0px 5px 30px 0px rgba(5, 0, 1, 0.05);
+    color: var(--white);
+    p {
+        color: #000;
+        font-family: Roboto;
+        font-size: 16px;
+        font-style: normal;
+        font-weight: 600;
+        line-height: 24px;
+    }
+    img {
+        width: 36px;
+        height: 36px;
+        border: 1px solid #f4f4f4;
+        border-radius: 50%;
+    }
+}
+.custom-toast {
+    background: rgba(0, 0, 0, 0) !important;
+    color: #fff;
+    font-size: 14px;
+    padding: 10px;
+    border-radius: 8px;
+    display: flex;
+    align-items: center;
+    gap: 8px;
 
+    .van-icon {
+        width: 24px;
+        height: 24px;
+    }
+    .van-icon--copy {
+        color: #fff;
+    }
+}
 </style>

+ 1 - 3
src/composables/fetch.ts

@@ -53,8 +53,6 @@ axios.interceptors.request.use(
 axios.interceptors.response.use(
     response => response,
     (error) => {
-        console.log(error,11);
-
         const response = {} as AxiosResponse
         response.config = error.config
         response.data = null
@@ -97,7 +95,7 @@ function checkCodeFn(data: ResponseData<any>) {
         }
     }
     else if (!code.includes(Number(data.code))) {
-        showToast(data.msg || '请求失败')
+        showToast(data.msg)
     }
     else {
         data.code = 200

+ 2 - 2
src/config/index.ts

@@ -17,7 +17,7 @@ switch (c) {
         // 开发环境
         Host00 = "https://testsecure.6cd7e0f0b52.com"
         // Host85= "https://testad.6cd7e0f0b52.com"
-        Host85 = "http://103.214.175.29:8700"
+        Host85 = "https://testucard.6cd7e0f0b52.com"
         // Host85= "http://192.168.0.33:8700"
         break;
 }
@@ -44,4 +44,4 @@ const config = {
     }
 }
 
-export default config
+export default config

+ 0 - 2
src/i18n/index.ts

@@ -28,8 +28,6 @@ const i18n = createI18n({
   // 添加全局注入
   globalInjection: true
 })
-console.log(i18n, 1000)
-
 // 监听语言变化
 watch(lang, (newLang) => {
   i18n.global.locale.value = newLang as LocaleType

+ 23 - 4
src/i18n/locales/cn.ts

@@ -7,7 +7,12 @@ export default {
         fail: '失败',
         input: '请输入',
         choose: '请选择',
-        country: '输入货币/国家'
+        country: '输入货币/国家',
+        error: '系统错误',
+        copy1: '卡号已复制!',
+        copy2: 'CVV已复制!',
+        copy3: '复制失败',
+
     },
     tabs: {
         wallet: '钱包',
@@ -53,6 +58,8 @@ export default {
         p12: '银行卡进度详细信息',
         p13: '持卡人姓名',
         p14: '到期日',
+        rechargeB1:'充值',
+        transferB2: '转账',
     },
     finance: {
         title: '金融',
@@ -81,7 +88,7 @@ export default {
         i1: '语言设置',
         i2: '修改密码',
         i3: '个人信息',
-        i4: '卡记录',
+        i4: '卡记录',
         i5: '关于PayouCard',
         i6: '退出登录',
     },
@@ -138,6 +145,7 @@ export default {
         all: '全部',
         recharge: '充值',
         fee: '手续费',
+        rate: '费率',
         records: '充值记录',
         walletPay: '钱包支付',
         rechargeSuccess: '充值成功',
@@ -146,6 +154,7 @@ export default {
         tips1: '您的充值订单已提交',
         tips2: '到账后,系统将自动处理您的充值。',
         tips3: '关闭',
+        maxAmountTip: '最大可充值金额为{max}'
     },
     'activate-card': {
         title: '卡片激活',
@@ -214,6 +223,8 @@ export default {
         postalCode: '邮政编码',
         submit: '提交',
         kycSuccess: '身份认证成功',
+        kycSuccess1: 'KYC认证已经提交,请等待审核!',
+        kycSuccess2: '开卡申请已经提交,开卡通过即可使用该卡!',
         kycFail: '身份认证失败',
         step1: '身份认证',
         step2: '支付',
@@ -262,7 +273,13 @@ export default {
         "item6": "基本信息",
         "item7": "收款银行ID",
         "item8": "用户唯一ID",
-        "item9": "速汇金额",
+        "item9": "速汇金额 (手续费率:{feeRate1}%)",
+        "item9_10": "速汇金额",
+        "item9_11": "手续费率",
+        "item9_12": "总手续费",
+        "item9_13": "{ allFee } USD({ feeRate2 } 比例 + { feeAmount1 } 固定)",
+        "item9_1": "总手续费:{ allFee } USD({ feeRate2 } 比例 + { feeAmount1 } 固定)",
+        maxAmountTip: '最大可速汇金额为{max}',
         "item10": "附言 (英文)",
         "item11": "收付款人关系",
         "item12": "兄弟",
@@ -354,6 +371,7 @@ export default {
         "item98": "支票账户",
         "item99": "储蓄账户",
         "item100": "定期存款账户",
+        'item100_1':'请在确认之前查看您的转账详细信息',
         "item101": "其它账户",
         "item102": "校验收款人",
         "item103": "校验付款人",
@@ -496,6 +514,7 @@ export default {
         p6: ' 银行',
         p7: '等',
         p8: '。',
+        p9: '您钱包余额不足,速汇最低金额{max}',
 
     },
     'remit-success': {
@@ -640,7 +659,7 @@ export default {
 
     },
     'apply-record': {
-        title: '银行卡申请记录'
+        title: '开卡记录'
     },
     'apply-record-detail': {
         title: '开卡详情',

+ 19 - 5
src/i18n/locales/en.ts

@@ -7,7 +7,11 @@ export default {
         fail: 'Failed',
         input: 'Please input',
         choose: 'Please choose',
-        country: 'Type a currency / country'
+        country: 'Type a currency / country',
+        error: 'System Error',
+        copy1: 'Card number copied!',
+        copy2: 'CVV copied!',
+        copy3: 'Copy failed',
     },
     tabs: {
         wallet: 'Wallet',
@@ -61,6 +65,8 @@ export default {
         p19: '',
         p20: '',
         p21: '',
+        rechargeB1:'Recharge',
+        transferB2: 'Transfer',
 
 
     },
@@ -91,7 +97,7 @@ export default {
         i1: 'Language Settings',
         i2: 'Change Password',
         i3: 'Personal Info',
-        i4: 'Card Purchase History',
+        i4: 'Card Opening Record',
         i5: 'About PayouCard',
         i6: 'Logout'
     },
@@ -158,6 +164,7 @@ export default {
         all: 'Use all',
         recharge: 'Recharge',
         fee: 'Recharge Fee',
+        rate: 'Recharge Rate',
         records: 'Recharge Records',
         walletPay: 'Wallet Payment',
         rechargeSuccess: 'Recharge Successful',
@@ -166,6 +173,7 @@ export default {
         tips1: 'Your top-up order has been submitted',
         tips2: 'Once the funds are received, the system will automatically process your top-up.',
         tips3: 'Close',
+        maxAmountTip: 'The maximum rechargeable amount is {max}'
     },
     'activate-card': {
         title: 'Activate Card',
@@ -234,6 +242,8 @@ export default {
         postalCode: 'Postal Code',
         submit: 'Submit',
         kycSuccess: 'KYC Verified',
+        kycSuccess1: 'KYC authentication has been submitted, please wait for review!',
+        kycSuccess2: 'The card application has been submitted, and once approved, the card can be used!',
         kycFail: 'KYC Failed',
         step1: 'Identity',
         step2: 'Payment',
@@ -278,7 +288,9 @@ export default {
         item6: 'Basic Info',
         item7: 'Beneficiary Bank ID',
         item8: 'User Unique ID',
-        item9: 'Remittance Amount',
+        item9: 'Remittance Amount (Fee rate:{feeRate1}%)',
+        "item9_1": "Fee: { allFee } USD ({ feeRate2 } variable + { feeAmount1 } fixed)",
+        maxAmountTip: 'The maximum amount that can be quickly remitted is {max}',
         item10: 'Note (English)',
         item11: 'Beneficiary Relationship',
         item12: 'Brother',
@@ -370,10 +382,11 @@ export default {
         item98: 'Checking Account',
         item99: 'Savings Account',
         item100: 'Fixed Deposit',
+        item100_1: 'Please review your transfer details before confirming',
         item101: 'Other Account',
         item102: 'Validate Beneficiary',
         item103: 'Validate Payer',
-        item104: 'Confirm Info',
+        item104: 'Review & Confirm',
         item105: 'Submit Remittance',
         item106: 'Order Number',
         item107: 'Merchant Order Number',
@@ -506,6 +519,7 @@ ms49: 'Length must be between 6 and 12 characters',
         p6: ' Banks',
         p7: 'wait',
         p8: '.',
+        p9: 'Your wallet balance is insufficient, the minimum amount for quick transfer is {max}.',
     },
     'remit-success': {
         title: 'Payment Successful',
@@ -646,7 +660,7 @@ ms49: 'Length must be between 6 and 12 characters',
         kycSuccess: 'Information Completed'
     },
     'apply-record': {
-        title: 'Bank card application record'
+        title: 'Card Opening Record'
     },
     'apply-record-detail': {
         title: 'Card opening details',

+ 1 - 1
src/views/apply-record.vue

@@ -197,7 +197,7 @@ onMounted(async () => {
             line-height: 20px;
             text-align: left;
             color: #1a1a1a;
-            width: 200px;
+            width: 180px;
         }
         .a2 {
             color: #6b7280;

+ 1 - 1
src/views/card-activation.vue

@@ -39,7 +39,7 @@ const getCardInfo = async () => {
             form.value = res.data
         }
     } catch (error) {
-        showToast(error || String(error))
+        showToast(t('common.error'))
     }
 }
 

+ 82 - 77
src/views/card-recharge.vue

@@ -3,7 +3,7 @@
         <div class="fixed-right" @click="goRechargeRecord">
             <icon name="icon_history" :size="24" color="#000" />
         </div>
-        <div v-if="rechargeCurrencyInfoList.length>0">
+        <div v-if="rechargeCurrencyInfoList.length > 0">
             <div class="g">
                 <img src="../assets/images/apply-record-1.png" alt="" />
                 <div class="g-l">
@@ -17,18 +17,18 @@
             </div>
             <div class="currency">
                 <remit-input
-                v-model:value="amount"
-                fkey="amount"
-                type="number"
-                :label="t('card-recharge.amount')"
-                :required="true"
-                :clearable="false"
-                @change="handleChange"
-                :placeholder="t('card-recharge.amountPlaceholder')"
-            >
-
-            </remit-input>
-            <div class="currency-r" @click="setModelValue">
+                    v-model:value="amount"
+                    fkey="amount"
+                    type="number"
+                    :label="t('card-recharge.amount')"
+                    :required="true"
+                    :clearable="false"
+                    :max="maxAmount"
+                    @change="handleChange"
+                    :placeholder="t('card-recharge.amountPlaceholder')"
+                >
+                </remit-input>
+                <div class="currency-r" @click="setModelValue">
                     <img :src="imageSrc(currencyT)" alt="" srcset="" />
                     <div class="r">
                         <p>{{ currencyT }}</p>
@@ -37,15 +37,19 @@
                 </div>
             </div>
 
-
             <div class="balance-info">
                 <span class="balance-key">{{ t('card-recharge.walletBalance') }}:</span>
                 <span class="balance-value">{{ balance }} {{ currency }}</span>
                 <span class="all-btn" @click="useAll">{{ t('card-recharge.all') }}</span>
             </div>
+            <div class="balance-info">
+                <span class="balance-key">{{ t('card-recharge.rate') }}:</span>
+                <span class="balance-value">{{ exchangeRate }}%</span>
+                <span></span>
+            </div>
             <div class="balance-info">
                 <span class="balance-key">{{ t('card-recharge.fee') }}:</span>
-                <span class="balance-value">{{ receivedAmount }} + {{ exchangeRate }}%</span>
+                <span class="balance-value">{{ fee }}</span>
                 <span></span>
             </div>
 
@@ -88,11 +92,15 @@ const { id } = route.query as { id: string }
 const cardInfo = ref<CardInfo | null>(null)
 const cardNo = ref('')
 const currency = ref('')
-const currencyT = ref('')
-const exchangeRate = ref(0)
+const currencyT = ref('USD')
+const exchangeRate = ref(1)
 const modelValue = ref(false)
-const receivedAmount = ref(0)
-const rechargeCurrencyInfoList = ref<{ text: string; value: string }[]>([])
+const rechargeCurrencyInfoList = ref<{ text: string; value: string }[]>([
+    {
+        text: '',
+        value: 'USD',
+    },
+])
 const isShow = ref(false)
 const btnClick = () => {
     isShow.value = false
@@ -100,8 +108,8 @@ const btnClick = () => {
 }
 const images = import.meta.glob('@/assets/images/currency/*.png', { eager: true })
 
-const imageSrc = (currency:string) => {
-  return images[`/src/assets/images/currency/${currency}.png`]?.default || fallbackImg
+const imageSrc = (currency: string) => {
+    return images[`/src/assets/images/currency/${currency}.png`]?.default || fallbackImg
 }
 
 const changeSelect = (e) => {
@@ -112,6 +120,10 @@ const setModelValue = () => {
 }
 const amount = ref('')
 const balance = ref(0)
+const maxAmount = computed(() => {
+    const rate = Number(exchangeRate.value) / 100
+    return Number((balance.value / (1 + rate)).toFixed(2))
+})
 const getCardInfo = async () => {
     if (!id) return
     try {
@@ -119,17 +131,12 @@ const getCardInfo = async () => {
         if (res.code === 200 && res.data) {
             cardInfo.value = res.data
             cardNo.value = cardInfo.value?.cardNo || ''
-            rechargeCurrencyInfoList.value = cardInfo.value?.rechargeCurrencyInfoList.map((item: { currency: string }) => ({
-                text: item.currency,
-                value: item.currency,
-            }))
-            currencyT.value = cardInfo.value?.rechargeCurrencyInfoList[0].currency || ''
             if (cardInfo.value?.cardNo) {
                 getBalance()
             }
         }
     } catch (error) {
-        showToast(error || String(error))
+        showToast(t('common.error'))
     }
 }
 
@@ -141,31 +148,39 @@ const goRechargeRecord = () => {
     router.push(`/recharge/record/list?cardNo=${cardInfo.value?.cardNo}`)
 }
 
-const canRecharge = computed(() => Number(amount.value) >= 10 && Number(amount.value) <= balance.value)
+const canRecharge = computed(() => {
+    const amt = Number(amount.value)
+    const feeVal = Number(fee.value)
+    return amt >= 10 && amt + feeVal <= balance.value
+})
 function useAll() {
-    amount.value = balance.value.toString()
+    const rate = Number(exchangeRate.value) / 100
+    amount.value = (balance.value / (1 + rate)).toFixed(2)
 }
 
+// 手续费
+const fee = computed(() => {
+    return Number(amount.value) === 0 ? 0 : ((Number(amount.value) * exchangeRate.value) / 100).toFixed(2)
+})
+
 // 充值
 async function handleRecharge() {
-    await estimateRecharge()
     try {
         const params = {
             cardNo: cardInfo.value?.cardNo,
             uniqueId: cardInfo.value?.uniqueId,
             amount: Number(amount.value),
-            currency: currencyT.value,
+            currency: currencyT.value + 'T',
         }
         const response = await ucardApi.ucardRecharge(params)
         if (response.code === 200) {
             isShow.value = true
             amount.value = ''
-            // getRechargeList()
         } else {
             showToast(response.msg)
         }
     } catch (error) {
-        showToast(error || String(error))
+        showToast(t('common.error'))
     }
 }
 const getBalance = async () => {
@@ -175,28 +190,20 @@ const getBalance = async () => {
         balance.value = res.data || 0
     } catch (error) {}
 }
-// 预估充值
-async function estimateRecharge() {
-    try {
-        const params = {
-            amount: Number(amount.value),
-            cardTypeId: cardInfo.value?.cardTypeId,
-            currency: currencyT.value,
-        }
-        const response = await ucardApi.ucardRechargeEstimate(params)
-        if (response.code === 200) {
-            exchangeRate.value = response.data.exchangeRate
-            receivedAmount.value = (response.data.receivedAmount * exchangeRate.value) / 100
-        } else {
-            showToast(response.msg)
-        }
-    } catch (error) {
-        showToast(error)
-    }
-}
 onMounted(() => {
     getCardInfo()
 })
+
+watch(amount, (val) => {
+    const num = Number(val)
+    const max = Number(maxAmount.value)
+    if (num > max) {
+        if (amount.value !== maxAmount.value) {
+            amount.value = maxAmount.value
+            showToast(t('card-recharge.maxAmountTip', { max: maxAmount.value }))
+        }
+    }
+})
 </script>
 
 <style scoped lang="scss">
@@ -364,36 +371,34 @@ onMounted(() => {
     color: #bdbdbd;
     font-size: 13px;
 }
-.currency{
+.currency {
     position: relative;
 }
 .currency-r {
-
     position: absolute;
     bottom: 2px;
     right: 0;
-        display: flex;
-        /* width: 164px; */
-        padding: 16px 16px 16px 10px;
-        height: 46px;
-        align-items: center;
-        gap: 12px;
-        border-radius: 12px;
-        box-shadow: 0px 5px 30px 0px rgba(5, 0, 1, 0.05);
-        color: var(--white);
-        p {
-            color: #000;
-            font-family: Roboto;
-            font-size: 16px;
-            font-style: normal;
-            font-weight: 600;
-            line-height: 24px;
-        }
-        img {
-            width: 32px;
-            height: 32px;
-            border: 1px solid #F4F4F4;
-            border-radius: 50%;
-        }
+    display: flex;
+    /* width: 164px; */
+    padding: 16px 16px 16px 10px;
+    height: 46px;
+    align-items: center;
+    gap: 12px;
+    border-radius: 12px;
+    color: var(--white);
+    p {
+        color: #000;
+        font-family: Roboto;
+        font-size: 16px;
+        font-style: normal;
+        font-weight: 600;
+        line-height: 24px;
     }
+    img {
+        width: 32px;
+        height: 32px;
+        border: 1px solid #f4f4f4;
+        border-radius: 50%;
+    }
+}
 </style>

+ 9 - 9
src/views/cards.vue

@@ -1,12 +1,13 @@
 <template>
-    <div class="add-apply" @click="addApply" v-if="kycList.length > 0">{{ t('cards.p10') }}</div>
-    <FirstApply v-if="cardList.length == 0" />
-    <VirtualCard v-else />
+    <div>
+        <div class="add-apply" @click="addApply" v-if="kycList.length > 0 && cardList.length > 0 ">{{ t('cards.p10') }}</div>
+        <FirstApply v-if="cardList.length == 0" />
+        <VirtualCard v-else />
+    </div>
 </template>
 
 <script setup lang="ts">
 import VirtualCard from '@/components/VirtualCard.vue'
-// import ApplyRecord from '@/components/ApplyRecord.vue'
 import FirstApply from '@/components/FirstApply.vue'
 import { ucardApi } from '@/api/ucard'
 import type { CardInfo } from '@/api/ucard'
@@ -43,9 +44,7 @@ const getCardList = async () => {
             data2.forEach((item: any) => {
                 const existing = mergedMap.get(item.cardTypeId) || {}
                 const { id, ...rest } = existing
-                console.log(id,121212);
-
-                mergedMap.set(item.cardTypeId, {  ...rest, ...item ,id})
+                mergedMap.set(item.cardTypeId, { ...rest, ...item, id })
             })
         }
         const mergedArray = Array.from(mergedMap.values())
@@ -68,9 +67,10 @@ const getCardList = async () => {
         })
         cardList.value = c
         kycList.value = b
+        console.log(c,'cardList');
+        console.log(b,'kycList');
+
         userStore.saveUserCard(cardList.value)
-        console.log(cardList.value, 'cardList.value')
-        console.log(kycList.value, 'kycList.value')
     } catch (error) {
     } finally {
         globalStore.setFullScreenLoading(false)

+ 210 - 71
src/views/eur-remit.vue

@@ -3,24 +3,25 @@
         <StepList :current-step="currentStep" :options="stepOptions" />
         <van-form ref="formRef" :model="paymentForm" class="payment-form" v-if="targetBankOptions.length > 0 && countryOptions.length > 0">
             <!-- 第一步:基本信息 -->
-            <div v-show="currentStep === 1" class="form-section">
+            <div v-show="currentStep === 3" class="form-section">
                 <h3 class="section-title">{{ t('eur-remit.item6') }}</h3>
-                <div class="bank-summary">
-                    <icon name="icon_bank" :size="48" color="#1a1a1a" />
-                    <div class="bank-r">
-                        <div class="bank-title">{{ t('eur.p5') }} {{ bankSummary?.number }}{{ t('eur.p6') }}</div>
-                        <div class="bank-value">{{ bankSummary?.value }}{{ bankSummary?.number > 5 ? t('eur.p7') : '' }}{{ t('eur.p8') }}</div>
+                <div class="amount-fee">
+                    <remit-input
+                        v-model:value="paymentForm.amount"
+                        type="number"
+                        fkey="amount"
+                        :label="t('eur-remit.item9', { feeRate1 })"
+                        @change="handleBankChange"
+                        :required="true"
+                        :rules="rules['amount']"
+                    />
+                    <div class="balance-info">
+                        <span class="balance-key">{{ t('card-recharge.walletBalance') }}:</span>
+                        <span class="balance-value">{{ balance }} USD</span>
+                        <span class="all-btn" @click="useAll">{{ t('card-recharge.all') }}</span>
                     </div>
+                    <div class="fee">{{ t('eur-remit.item9_1', { allFee, feeRate2, feeAmount1 }) }}</div>
                 </div>
-                <remit-input
-                    v-model:value="paymentForm.amount"
-                    type="number"
-                    fkey="amount"
-                    :label="t('eur-remit.item9')"
-                    @change="handleBankChange"
-                    :required="true"
-                    :rules="rules['amount']"
-                />
                 <remit-input
                     v-model:value="paymentForm.postscript"
                     type="text"
@@ -204,9 +205,15 @@
                 />
             </div>
             <!-- 第三步:收款人信息 -->
-            <div v-show="currentStep === 3" class="form-section">
+            <div v-show="currentStep === 1" class="form-section">
                 <h3 class="section-title">{{ t('eur-remit.item73') }}</h3>
-
+                <div class="bank-summary">
+                    <icon name="icon_bank" :size="48" color="#1a1a1a" />
+                    <div class="bank-r">
+                        <div class="bank-title">{{ t('eur.p5') }} {{ bankSummary?.number }}{{ t('eur.p6') }}</div>
+                        <div class="bank-value">{{ bankSummary?.value }}{{ bankSummary?.number > 5 ? t('eur.p7') : '' }}{{ t('eur.p8') }}</div>
+                    </div>
+                </div>
                 <remit-input
                     v-model:value="paymentForm.bankId"
                     type="select"
@@ -408,15 +415,23 @@
             </div>
             <!-- 第四步:确认信息 -->
             <div v-if="currentStep === 4" class="form-section">
-                <h3 class="section-title">{{ t('eur-remit.item100') }}</h3>
-                <p class="section-title1">Please review your transfer details before confirming</p>
+                <h3 class="section-title">{{ t('eur-remit.item104') }}</h3>
+                <h3 class="section-title1">{{ t('eur-remit.item100_1') }}</h3>
                 <div class="confirm-info">
                     <div class="info-group">
                         <h4>{{ t('eur-remit.item6') }}</h4>
                         <div class="info-item">
-                            <span class="label">{{ t('eur-remit.item9') }}</span>
+                            <span class="label">{{ t('eur-remit.item9_10') }}</span>
                             <p class="value">{{ formData.amount }}</p>
                         </div>
+                        <div class="info-item">
+                            <span class="label">{{ t('eur-remit.item9_11') }}</span>
+                            <p class="value">{{ feeRate1 }}%</p>
+                        </div>
+                        <div class="info-item">
+                            <span class="label">{{ t('eur-remit.item9_12') }}</span>
+                            <p class="value">{{ t('eur-remit.item9_13', { allFee, feeRate2, feeAmount1 }) }}</p>
+                        </div>
                         <div class="info-item">
                             <span class="label">{{ t('eur-remit.item10') }}</span>
                             <p class="value">{{ formData.postscript }}</p>
@@ -453,9 +468,12 @@
 
             <!-- 步骤控制按钮 -->
             <div class="form-actions">
-                <template v-if="currentStep === 1">
+                <template v-if="currentStep === 3">
                     <div class="fixed-btn">
                         <div class="cwg-button two-btn">
+                            <van-button plain block @click="goStep(2)" class="prev-btn">
+                                {{ t('eur-remit.prev') }}
+                            </van-button>
                             <van-button type="primary" block @click="validateStep1" :loading="loadingStates.validateStep1">
                                 {{ t('eur-remit.next') }}</van-button
                             >
@@ -474,12 +492,9 @@
                         </div>
                     </div>
                 </template>
-                <template v-else-if="currentStep === 3">
+                <template v-else-if="currentStep === 1">
                     <div class="fixed-btn">
                         <div class="cwg-button two-btn">
-                            <van-button plain block @click="goStep(2)" class="prev-btn">
-                                {{ t('eur-remit.prev') }}
-                            </van-button>
                             <van-button type="warning" block @click="validateStep3" :loading="loadingStates.validateStep3">
                                 {{ t('eur-remit.next') }}
                             </van-button>
@@ -512,7 +527,7 @@ import { ucardApi } from '@/api/ucard'
 import RemitInput from '@/components/RemitInput.vue'
 import { lang } from '@/composables/config'
 const route = useRoute()
-const { currency } = route.query as { currency: string }
+const { currency, allAmount } = route.query as { currency: string; allAmount: string }
 const { t } = useI18n()
 import useUserStore from '@/stores/use-user-store'
 import { ucardValidateParams } from '@/api/ucard'
@@ -552,46 +567,46 @@ const isAllValidated = ref(false)
 const formData = ref<ucardValidateParams>({ uniqueId: userInfo?.uniqueId } as ucardValidateParams)
 // 表单数据
 const paymentForm = ref({
-    // bankId: 10000,
-    // uniqueId: userInfo?.uniqueId,
-    // originOrderNo: '',
-    // amount: 200,
-    // postscript: 'testorder',
-    // relationship: 'OTHER',
-    // sourceFunds: 'SAVINGS',
-    // payPurpose: 'INTERNATIONAL_TRADE',
-    // payerType: 'INDIVIDUAL',
-    // payerLastName: 'zhang',
-    // payerFirstName: 'tom',
-    // payerIdNo: 'ES125664',
-    // payerIdNoType: 'PASSPORT',
-    // payerIdCountry: 'SG',
-    // payerBirthday: '1990-10-10',
-    // payerNationalityCountry: 'SG',
-    // payerMobile: '+65012345678',
-    // payerCountryCode: 'SG',
-    // payerCityCode: 'SG_1',
-    // payerAddress: '21 Tampines Ave 1, Singapore 529757',
-    // payerPostCode: '999002',
-    // payerOccupation: 'worker',
-    // benAccountNum: '235486845630',
-    // benAccountName: 'wangfei',
-    // benCountryCode: 'SG',
-    // benCityCode: 'SG_1',
-    // benAddress: '21 Tampines Ave 1, Singapore 529757',
-    // benPostCode: '999002',
-    // benTransBankSwift: '',
-    // benLastName: 'zhang',
-    // benFirstName: 'alex',
-    // benNationalityCountry: 'SG',
-    // benIdNoType: 'PASSPORT',
-    // benIdNo: '1334924322424',
-    // benIdExpirationDate: '2030-01-01',
-    // benBirthday: '2001-01-01',
-    // benMobileCode: '+67',
-    // benMobile: '18326559132',
-    // benBankAccountType: 'SAVINGS',
-    // benBankCode: '',
+    bankId: 10000,
+    uniqueId: userInfo?.uniqueId,
+    originOrderNo: '',
+    amount: 200,
+    postscript: 'testorder',
+    relationship: 'OTHER',
+    sourceFunds: 'SAVINGS',
+    payPurpose: 'INTERNATIONAL_TRADE',
+    payerType: 'INDIVIDUAL',
+    payerLastName: 'zhang',
+    payerFirstName: 'tom',
+    payerIdNo: 'ES125664',
+    payerIdNoType: 'PASSPORT',
+    payerIdCountry: 'SG',
+    payerBirthday: '1990-10-10',
+    payerNationalityCountry: 'SG',
+    payerMobile: '+65012345678',
+    payerCountryCode: 'SG',
+    payerCityCode: 'SG_1',
+    payerAddress: '21 Tampines Ave 1, Singapore 529757',
+    payerPostCode: '999002',
+    payerOccupation: 'worker',
+    benAccountNum: '235486845630',
+    benAccountName: 'wangfei',
+    benCountryCode: 'SG',
+    benCityCode: 'SG_1',
+    benAddress: '21 Tampines Ave 1, Singapore 529757',
+    benPostCode: '999002',
+    benTransBankSwift: '',
+    benLastName: 'zhang',
+    benFirstName: 'alex',
+    benNationalityCountry: 'SG',
+    benIdNoType: 'PASSPORT',
+    benIdNo: '1334924322424',
+    benIdExpirationDate: '2030-01-01',
+    benBirthday: '2001-01-01',
+    benMobileCode: '+67',
+    benMobile: '18326559132',
+    benBankAccountType: 'SAVINGS',
+    benBankCode: '',
 })
 
 // 表单验证规则
@@ -599,7 +614,21 @@ const rules = {
     bankId: [{ required: true, message: t('eur-remit.ms3') }],
     amount: [
         { required: true, message: t('eur-remit.ms6') },
-        { pattern: /^(?:100|\d{3,})(?:\.\d{1,2})?$/, message: t('eur-remit.ms') },
+        {
+            validator: (val: string) => {
+                const num = Number(val)
+                if (isNaN(num)) return t('eur-remit.ms')
+                if (!/^\d+(\.\d{1,2})?$/.test(val)) return t('eur-remit.ms')
+                if (num < 100) return t('eur-remit.ms', { min: 100 })
+                if (maxAmount.value > limitAmount.value) {
+                    if (num > limitAmount.value) return t('eur-remit.maxAmountTip', { max: limitAmount.value })
+                } else {
+                    if (num > maxAmount.value) return t('eur-remit.maxAmountTip', { max: maxAmount.value })
+                }
+                return true
+            },
+            trigger: 'blur',
+        },
     ],
     postscript: [
         { required: true, message: t('eur-remit.ms7') },
@@ -817,7 +846,7 @@ const getMobileCode = async () => {
             value: item.callingCode,
         }))
     } catch (error) {
-        showToast(error || String(error))
+        showToast(t('common.error'))
     }
 }
 // 银行账户类型
@@ -828,6 +857,32 @@ const bankAccountTypeOptions = ref([
 
 const bankConfigList = ref<any[]>([])
 const payeeParams1 = ref<string[]>([])
+const feeAmount1 = ref<number>(0)
+const feeRate1 = ref<number>(0)
+const maxAmount = computed(() => {
+    const rate = Number(feeRate1.value) / 100
+    return Number((balance.value / (1 + rate) - feeAmount1.value).toFixed(2))
+})
+const limitAmount = computed(() => {
+    return currency == 'CNY' ? 29999 : 1000000
+})
+
+function useAll() {
+    const rate = Number(feeRate1.value) / 100
+    paymentForm.value.amount = (balance.value / (1 + rate) - feeAmount1.value).toFixed(2)
+}
+// 手续费
+const feeRate2 = computed(() => {
+    const amount = formData.value.amount
+    return Number(amount) === 0 ? 0 : ((Number(amount) * feeRate1.value) / 100).toFixed(2)
+})
+const allFee = computed(() => {
+    return Number(feeRate2.value) + feeAmount1.value
+})
+const balance = computed(() => {
+    return Number(allAmount)
+})
+const paymentType1 = ref(15)
 const showField = (key: string) =>
     computed(() => {
         return payeeParams1.value.includes(key)
@@ -907,6 +962,22 @@ const getCityListForSelect = async (countryCode: string, type: 'payer' | 'payee'
         }
     }
 }
+// 汇率
+const getUcardRate = async () => {
+    try {
+        const res = await ucardApi.ucardRate({
+            currency: 'USD',
+            targetCurrency: 'CNY',
+            targetCountry: 'CN',
+        })
+        if (res.code === 200 || res.code === 0) {
+            console.log(1122,res);
+            
+        }
+    } catch (error) {
+       
+    }
+}
 
 // API 调用函数
 interface ApiResponse {
@@ -1006,7 +1077,7 @@ const validateStep1 = async () => {
     loadingStates.value.validateStep1 = true
     try {
         await formRef.value?.validate(['amount', 'postscript'])
-        goStep(2)
+        goStep(4)
     } catch (error) {
         if (Array.isArray(error) && error.length > 0) {
             showToast(error[0].message)
@@ -1067,7 +1138,7 @@ const validateStep3 = async () => {
         await formRef.value?.validate([...requiredFields, ...payeeParams1.value])
         const response = await validatePayeeInfo(formData.value.payee)
         if (response.success) {
-            goStep(4)
+            goStep(2)
             isAllValidated.value = false
         } else {
             showToast(response.message || t('eur-remit.validateFailed'))
@@ -1165,10 +1236,28 @@ const handleBankChange = (value: any) => {
         'benBankAccountType',
         'benBankCode',
     ]
+    if (value.key === 'amount') {
+        const num = Number(value.value)
+        const max = Number(maxAmount.value)
+        if (maxAmount.value > limitAmount.value) {
+            if (num > limitAmount.value) return t('eur-remit.maxAmountTip', { max: limitAmount.value })
+        } else {
+            if (num > max) {
+                if (value.value !== maxAmount.value) {
+                    showToast(t('eur-remit.maxAmountTip', { max: maxAmount.value }))
+                }
+            }
+        }
+    }
     if (value.key === 'bankId') {
         formData.value.bankId = value.value
-        const { payeeParams } = bankConfigList.value.find((b) => b.bankId === value.value)
+        const { payeeParams, feeAmount, feeRate, paymentType } = bankConfigList.value.find((b) => b.bankId === value.value)
+
+        console.log(paymentType, feeAmount, feeRate, 1999)
         payeeParams1.value = payeeParams
+        feeAmount1.value = feeAmount
+        feeRate1.value = feeRate
+        paymentType1.value = paymentType
         // payeeParams1.value = [
         //     'benLastName',
         //     'benFirstName',
@@ -1229,6 +1318,7 @@ watch(formData, saveCache, { deep: true })
 
 onMounted(() => {
     loadCache()
+    getUcardRate()
     loadBankConfigList()
     getCountryListForSelect()
     getMobileCode()
@@ -1312,6 +1402,55 @@ onBeforeRouteLeave(() => {
 
 .form-section {
     margin: 8px 0;
+    .amount-fee {
+        .form-group {
+            margin-bottom: 0;
+        }
+        .fee {
+            color: #8e8a8a;
+            font-family: Roboto;
+            font-size: 14px;
+            font-style: normal;
+            font-weight: 400;
+            line-height: 20px;
+            letter-spacing: 0.07px;
+            margin: 16px 0;
+        }
+        .balance-info {
+            width: 100%;
+            color: #bdbdbd;
+            font-size: 14px;
+            margin: 16px 0;
+            display: flex;
+            align-items: center;
+        }
+        .balance-value {
+            width: 130px;
+            color: var(--white);
+            font-weight: bold;
+            margin: 0 8px;
+        }
+        .balance-key {
+            color: #8e8a8a;
+            width: 130px;
+        }
+        .all-btn {
+            display: flex;
+            padding: 4px 12px;
+            justify-content: center;
+            align-items: center;
+            gap: 10px;
+            border-radius: 35px;
+            border: 1px solid #ea002a;
+            color: var(--Brand-color, #ea002a);
+            font-family: Roboto;
+            font-size: 14px;
+            font-style: normal;
+            font-weight: 600;
+            line-height: 20px;
+            letter-spacing: 0.07px;
+        }
+    }
 }
 
 .section-title {

+ 1 - 1
src/views/eur.vue

@@ -55,7 +55,7 @@ const getTransferList = async () => {
             transferList.value = []
         }
     } catch (error) {
-        showToast(error || String(error))
+        showToast(t('common.error'))
     }
 }
 const goToTransferDetail = (id: string) => {

+ 24 - 8
src/views/home.vue

@@ -76,11 +76,14 @@ const balance = ref('USD')
 const modelValue = ref(false)
 const modelValue1 = ref(false)
 const currencyList = ref([])
+const allAmount = ref(0)
 const bankConfigList = ref([])
 const getBalance = async () => {
     try {
         const res = await userApi.walletBalance()
         const { amount, decimal } = splitNumber(res.data)
+
+        allAmount.value = res.data
         currencyList.value.push({
             name: 'USD',
             amount,
@@ -90,17 +93,25 @@ const getBalance = async () => {
 }
 const images = import.meta.glob('@/assets/images/currency/*.png', { eager: true })
 
-console.log(images,122);
-
-const imageSrc = (currency:string) => {
-  return images[`/src/assets/images/currency/${currency}.png`]?.default || fallbackImg
+const imageSrc = (currency: string) => {
+    return images[`/src/assets/images/currency/${currency}.png`]?.default || fallbackImg
 }
 
 const splitNumber = (n) => {
-    if (!n) {
+    if (
+        n === undefined ||
+        n === null ||
+        n === 0 ||
+        (typeof n !== 'number' && typeof n !== 'string') ||
+        (typeof n === 'string' && !/^\d+(\.\d+)?$/.test(n))
+    ) {
         return { amount: 0, decimal: '' }
     }
-    const [int, dec] = n.toString().split('.')
+    const str = n.toString()
+    if (!str.includes('.')) {
+        return { amount: str, decimal: '' }
+    }
+    const [int, dec] = str.split('.')
     return {
         amount: int + '.',
         decimal: dec || '',
@@ -115,7 +126,12 @@ const setModelValue1 = () => {
     modelValue1.value = true
 }
 const goToGlobalRemit = () => {
-    router.push('/eur/remit?currency=' + currency.value + '&currentStep=1')
+    if (allAmount.value >= 100) {
+        router.push('/eur/remit?currency=' + currency.value + '&allAmount=' + allAmount.value+ '&currentStep=1')
+        return
+    } else {
+        showToast(t('eur.p9', { max: 100 }))
+    }
 }
 const changeSelect = (e) => {
     currency.value = e.value
@@ -146,7 +162,7 @@ const getTransferList = async () => {
             transferList.value = []
         }
     } catch (error) {
-        showToast(error || String(error))
+        showToast(t('common.error'))
     }
 }
 const goToTransferDetail = (id: string) => {

+ 1 - 395
src/views/improve-info.vue

@@ -1,397 +1,3 @@
 <template>
-    <div class="page page-shadow">
-        <van-form ref="formRef" class="kyc-form" v-if="isShow && countryOptions.length > 0">
-            <remit-input
-                v-model:value="infoForm.lastName"
-                fkey="lastName"
-                :required="true"
-                :label="t('improve-info.p8')"
-                :rules="rules['lastName']"
-                @change="handleChange"
-            />
-            <remit-input
-                v-model:value="infoForm.firstName"
-                fkey="firstName"
-                :required="true"
-                :label="t('improve-info.p9')"
-                :rules="rules['firstName']"
-                @change="handleChange"
-            />
-
-            <remit-input
-                v-model:value="infoForm.email"
-                fkey="email"
-                :label="t('improve-info.p7')"
-                :required="true"
-                :rules="rules['email']"
-                @change="handleChange"
-            />
-            <div class="f">
-                <remit-input
-                    class="l"
-                    v-model:value="infoForm.areaCode"
-                    v-if="phoneCodes.length > 0"
-                    fkey="areaCode"
-                    :required="true"
-                    type="select"
-                    :show-search="true"
-                    :columns="phoneCodes"
-                    :label="t('improve-info.p6')"
-                    :rules="rules['areaCode']"
-                    @change="handleChange"
-                />
-                <remit-input class="r" v-model:value="infoForm.mobile" fkey="mobile" label=" " :rules="rules['mobile']" @change="handleChange" />
-            </div>
-            <remit-input
-                v-model:value="infoForm.sex"
-                fkey="sex"
-                type="select"
-                :columns="sexOptions"
-                :label="t('improve-info.p13')"
-                @change="handleChange"
-            />
-            <remit-input v-model:value="infoForm.birthday" type="date" fkey="birthday" :label="t('improve-info.p10')" @change="handleChange" />
-            <remit-input
-                v-model:value="infoForm.nationality"
-                fkey="nationality"
-                type="select"
-                :show-search="true"
-                :columns="countryOptions"
-                :label="t('improve-info.p17')"
-                @change="handleChange"
-            />
-            <remit-input
-                v-model:value="infoForm.town"
-                fkey="town"
-                :show-search="true"
-                type="select"
-                :columns="cityOptions"
-                :label="t('improve-info.p19')"
-                @change="handleChange"
-            />
-            <remit-input
-                v-model:value="infoForm.address"
-                fkey="address"
-                :label="t('improve-info.p21')"
-                :required="true"
-                :rules="rules['address']"
-                @change="handleChange"
-            />
-            <remit-input v-model:value="infoForm.postCode" fkey="postCode" :label="t('improve-info.p22')" @change="handleChange" />
-            <remit-input
-                v-model:value="infoForm.idType"
-                fkey="idType"
-                type="select"
-                :columns="idTypeOptions"
-                :label="t('improve-info.p23')"
-                @change="handleChange"
-            />
-            <remit-input v-model:value="infoForm.idNo" fkey="idNo" :label="t('improve-info.p27')" @change="handleChange" />
-            <remit-input
-                v-model:value="infoForm.idNoExpiryDate"
-                type="date"
-                fkey="idNoExpiryDate"
-                :label="t('improve-info.p28')"
-                @change="handleChange"
-            />
-            <remit-input
-                v-model:value="infoForm.idPicture"
-                type="upload"
-                fkey="idPicture"
-                :label="t('improve-info.p29')"
-                :isUploadD="true"
-                accept="image/png, image/jpeg, image/jpg"
-                @change="handleChange"
-            >
-                <div class="cwg-upload">
-                    <van-icon name="back-top" />
-                    <p class="name">{{ t('improve-info.p28_1') }}{{ t('improve-info.p29') }}</p>
-                    <p class="back">{{ t('improve-info.p28_2') }}</p>
-                </div>
-            </remit-input>
-            <remit-input
-                v-model:value="infoForm.facePicture"
-                type="upload"
-                fkey="facePicture"
-                :label="t('improve-info.p30')"
-                :isUploadD="true"
-                accept="image/png, image/jpeg, image/jpg"
-                @change="handleChange"
-            >
-                <div class="cwg-upload">
-                    <van-icon name="back-top" />
-                    <p class="name">{{ t('improve-info.p28_1') }}{{ t('improve-info.p30') }}</p>
-                    <p class="back">{{ t('improve-info.p28_2') }}</p>
-                </div>
-            </remit-input>
-            <div class="fixed-btn">
-                <div class="cwg-button">
-                    <van-button type="primary" block @click="kycSubmit">{{ t('improve-info.p33') }}</van-button>
-                </div>
-            </div>
-        </van-form>
-    </div>
+    <KycInfo />
 </template>
-
-<script setup lang="ts">
-import { pinyin } from 'pinyin-pro'
-import { useRouter } from 'vue-router'
-import { useI18n } from 'vue-i18n'
-import { showToast } from 'vant'
-import { ucardApi } from '@/api/ucard'
-import { userApi } from '@/api/user'
-import useUserStore from '@/stores/use-user-store'
-const userStore = useUserStore()
-const userInfo = computed(() => userStore.userInfo)
-const { t } = useI18n()
-const router = useRouter()
-const route = useRoute()
-const formRef = ref()
-const { type } = route.query as { type: string }
-const sexOptions = ref([
-    { text: t('improve-info.p15'), value: '1' },
-    { text: t('improve-info.p16'), value: '2' },
-])
-const idTypeOptions = ref([
-    { text: t('improve-info.p25'), value: 'EUROPEAN_ID' },
-    { text: t('improve-info.p26'), value: 'PASSPORT' },
-])
-// 表单验证规则
-const rules = {
-    email: [{ required: true, message: t('improve-info.p3_3_5') }],
-    lastName: [{ required: true, message: t('improve-info.p3_1') }],
-    firstName: [{ required: true, message: t('improve-info.p3_2') }],
-    address: [{ required: true, message: t('improve-info.p3_4') }],
-    mobile: [{ required: true, message: t('improve-info.p3_3') }],
-    areaCode: [{ required: true, message: t('improve-info.p5') }],
-}
-const infoForm = ref({
-    areaCode: undefined,
-    country: undefined,
-    mobile: undefined,
-    idNo: undefined,
-    sex: undefined,
-    birthday: undefined,
-    nationality: undefined,
-    town: undefined,
-    postCode: undefined,
-    idType: undefined,
-    idNoExpiryDate: undefined,
-    idPicture: undefined,
-    facePicture: undefined,
-    email: undefined,
-    lastName: undefined,
-    firstName: undefined,
-    address: undefined,
-    cId: undefined,
-})
-const formData = ref<typeof infoForm.value>({} as any)
-const formDatas = ref<typeof infoForm.value>({} as any)
-const isShow = ref(false)
-
-// 国家选项
-const countryOptions = ref<Array<{ text: string; value: string }>>([])
-const cityOptions = ref<Array<{ text: string; value: string }>>([])
-const phoneCodes = ref([])
-// 获取国家列表
-const getCountryListForSelect = async () => {
-    try {
-        const res = await ucardApi.ucardCountryCity({})
-        if (res.code === 200 || res.code === 0) {
-            countryOptions.value = res.data.map((item: any) => ({
-                text: lang.value === 'cn' ? item.cnName : item.enName,
-                value: item.code,
-            }))
-        }
-    } catch (error) {
-        countryOptions.value = []
-    }
-}
-// 获取城市列表
-const getCityListForSelect = async (countryCode: string) => {
-    try {
-        const res = await ucardApi.ucardCountryCity({ code: countryCode })
-        if (res.code === 200 || res.code === 0) {
-            const cityList = res.data.map((item: any) => ({
-                text: lang.value === 'cn' ? item.cnName : item.enName,
-                value: item.code,
-            }))
-            cityOptions.value = cityList
-        }
-    } catch (error) {
-        cityOptions.value = []
-    }
-}
-const getCountry = async () => {
-    try {
-        const res = await ucardApi.countryGet()
-        phoneCodes.value = res.data.map((item: { callingCode: string; name: string; enName: string }) => ({
-            text: lang.value === 'cn' ? item.name + ' + ' + item.callingCode : item.enName + ' + ' + item.callingCode,
-            value: item.callingCode,
-        }))
-    } catch (error) {
-        showToast(error || String(error))
-    }
-}
-const kycSubmit = async () => {
-
-    try {
-        const requiredFields = ['email', 'lastName', 'firstName', 'address', 'mobile', 'areaCode'] as const
-    await formRef.value?.validate(requiredFields)
-
-
-    } catch (error) {
-        if (Array.isArray(error) && error.length > 0) {
-            showToast(error[0].message)
-        } else {
-            showToast(t('improve-info.errer'))
-        }
-        return
-
-    }
-
-    let res
-    if (formData.value.uniqueId) {
-        res = await ucardApi.merchantUpdate(formData.value as any)
-        if (res.code === 200) {
-            showToast(t('improve-info.kycSuccess'))
-            router.push('/mine')
-        }
-    } else {
-        res = await ucardApi.merchantRegister(formData.value as any)
-        if (res.code === 200) {
-            showToast(t('improve-info.kycSuccess'))
-            userInfo.value.customInfo.uniqueId = res.data
-            userStore.saveUserInfo(userInfo.value)
-            router.push('/')
-        }
-    }
-
-}
-const handleChange = (value: any) => {
-    formData.value = { ...formData.value, [value.key]: value.value }
-    if (value.key === 'nationality') {
-        formData.value = { ...formData.value, country: value.value }
-        getCityListForSelect(value.value)
-        formData.value.town = ''
-    }
-}
-const containsChinese = (str: string) => /[\u4e00-\u9fa5]/.test(str)
-const convertToPinyin = (value: string) => (containsChinese(value) ? pinyin(value, { toneType: 'none', type: 'capitalize' }) : value)
-const getInfoForm = () => {
-    const customInfo = userInfo.value?.customInfo || {}
-    const fields = [
-        'areaCode',
-        'mobile',
-        'idNo',
-        'sex',
-        'birthday',
-        'nationality',
-        'town',
-        'postCode',
-        'idType',
-        'idNoExpiryDate',
-        'idPicture',
-        'facePicture',
-        'email',
-        'lastName',
-        'firstName',
-        'address',
-        'cId',
-        'uniqueId',
-    ]
-
-    const data: Record<string, any> = {}
-    for (const key of fields) {
-        data[key] = customInfo[key] ?? undefined
-    }
-    if (customInfo.uniqueId) {
-        for (const key of fields) {
-            data[key] = formDatas.value[key] ?? undefined
-        }
-    }
-    data.lastName = convertToPinyin(data.lastName)
-    data.firstName = convertToPinyin(data.firstName)
-    infoForm.value = { ...data }
-    formData.value = { ...formData.value, ...data }
-    console.log(formData.value, 1222)
-}
-const getUserInfo = async () => {
-    if (!userToken.value) {
-        showToast('请先登录')
-        return
-    }
-    try {
-        const res = await userApi.getUserSingle()
-        if (res.code === 200 && res.data.uniqueId) {
-            formDatas.value = res.data
-            getInfoForm()
-        }
-    } catch (error: any) {}
-}
-onMounted(async () => {
-    if (type == '1') {
-        getUserInfo()
-    } else {
-        getInfoForm()
-    }
-    isShow.value = false
-    await getCountry()
-    await getCountryListForSelect()
-    if (infoForm.value.nationality) {
-        await getCityListForSelect(infoForm.value.nationality)
-        isShow.value = true
-    } else {
-        isShow.value = true
-    }
-})
-</script>
-
-<style scoped lang="scss">
-.f {
-    display: flex;
-    align-items: flex-end;
-    gap: 12px;
-    .l {
-        flex: 1;
-    }
-    .r {
-        width: 273px;
-    }
-}
-
-:deep(.van-uploader) {
-    width: 100%;
-    .van-uploader__wrapper {
-        width: 100%;
-        display: block;
-    }
-}
-::v-deep(.van-uploader__preview) {
-    width: 100% !important;
-    height: 160px !important;
-    border: 1px dashed #beb6b6;
-    border-radius: 24px;
-    overflow: hidden;
-    .van-uploader__preview-image {
-        width: 100%;
-        height: 100%;
-        .van-image__img {
-            object-fit: contain;
-        }
-    }
-    .van-uploader__preview-delete {
-        position: absolute;
-        top: 0;
-        right: 0;
-        width: 30px;
-        height: 30px;
-        border-radius: 0 24px 0 0;
-        i {
-            text-align: center;
-            line-height: 30px;
-            font-size: 30px;
-        }
-    }
-}
-</style>

+ 1 - 605
src/views/kyc.vue

@@ -1,607 +1,3 @@
 <template>
-    <div class="page page-shadow">
-        <div class="kyc-form" v-if="isShow && countryOptions.length > 0">
-            <remit-input
-                v-model:value="infoForm.lastName"
-                fkey="lastName"
-                :required="true"
-                :disabled="true"
-                :label="t('improve-info.p8')"
-                :rules="rules['lastName']"
-                @change="handleChange"
-            />
-            <remit-input
-                v-model:value="infoForm.firstName"
-                fkey="firstName"
-                :required="true"
-                :disabled="true"
-                :label="t('improve-info.p9')"
-                :rules="rules['firstName']"
-                @change="handleChange"
-            />
-
-            <remit-input
-                v-model:value="infoForm.email"
-                fkey="email"
-                :label="t('improve-info.p7')"
-                :required="true"
-                :disabled="true"
-                :rules="rules['email']"
-                @change="handleChange"
-            />
-            <remit-input
-                v-model:value="infoForm.areaCode"
-                v-if="phoneCodes.length > 0"
-                fkey="areaCode"
-                :required="true"
-                :disabled="true"
-                type="select"
-                :show-search="true"
-                :columns="phoneCodes"
-                :label="t('improve-info.p4')"
-                :rules="rules['areaCode']"
-                @change="handleChange"
-            />
-            <remit-input
-                v-model:value="infoForm.mobile"
-                fkey="mobile"
-                :label="t('improve-info.p6')"
-                :required="true"
-                :disabled="true"
-                :rules="rules['mobile']"
-                @change="handleChange"
-            />
-            <remit-input
-                v-model:value="infoForm.sex"
-                fkey="sex"
-                type="select"
-                :disabled="true"
-                :columns="sexOptions"
-                :label="t('improve-info.p13')"
-                @change="handleChange"
-            />
-            <remit-input v-model:value="infoForm.birthday" :disabled="true" type="date" fkey="birthday" :label="t('improve-info.p10')" @change="handleChange" />
-            <remit-input
-                v-model:value="infoForm.nationality"
-                fkey="nationality"
-                type="select"
-                :show-search="true"
-                :disabled="true"
-                :columns="countryOptions"
-                :label="t('improve-info.p17')"
-                @change="handleChange"
-            />
-            <remit-input
-                v-model:value="infoForm.town"
-                fkey="town"
-                :show-search="true"
-                :disabled="true"
-                type="select"
-                :columns="cityOptions"
-                :label="t('improve-info.p19')"
-                @change="handleChange"
-            />
-            <remit-input
-                v-model:value="infoForm.address"
-                fkey="address"
-                :disabled="true"
-                :label="t('improve-info.p21')"
-                :required="true"
-                :rules="rules['address']"
-                @change="handleChange"
-            />
-            <remit-input v-model:value="infoForm.postCode" :disabled="true" fkey="postCode" :label="t('improve-info.p22')" @change="handleChange" />
-            <remit-input
-                v-model:value="infoForm.type"
-                type="select"
-                fkey="type"
-                :label="t('kyc.type')"
-                :required="true"
-                :rules="[{ required: true, message: t('kyc.type') }]"
-                @change="handleChange"
-                :columns="typeOptions"
-            />
-            <remit-input
-                v-model:value="infoForm.data"
-                type="upload"
-                fkey="data"
-                :label="t('kyc.data')"
-                :isUploadD="true"
-                :required="true"
-                accept="image/png, image/jpeg, image/jpg"
-                @change="handleChange"
-            >
-                <div class="cwg-upload">
-                    <van-icon name="back-top" />
-                    <p class="name">{{t('improve-info.p28_1')}}{{ t('kyc.data') }}</p>
-                    <p class="back">{{t('improve-info.p28_2')}}</p>
-                </div>
-            </remit-input>
-
-            <div class="fixed-btn">
-                <div class="cwg-button">
-                    <van-button type="primary" block @click="handleSubmit">{{ t('kyc.submit') }}</van-button>
-                </div>
-            </div>
-        </div>
-    </div>
+    <KycInfo />
 </template>
-
-<script setup lang="ts">
-import { pinyin } from 'pinyin-pro'
-import { useRouter } from 'vue-router'
-import { useI18n } from 'vue-i18n'
-import { showToast } from 'vant'
-import { ucardApi } from '@/api/ucard'
-import { userApi } from '@/api/user'
-import useUserStore from '@/stores/use-user-store'
-const userStore = useUserStore()
-const userInfo = computed(() => userStore.userInfo)
-const { t } = useI18n()
-const router = useRouter()
-const route = useRoute()
-const { cardTypeId } = route.query as { cardTypeId: string; }
-
-const sexOptions = ref([
-    { text: t('improve-info.p15'), value: '1' },
-    { text: t('improve-info.p16'), value: '2' },
-])
-// 表单验证规则
-const rules = {
-    email: [{ required: true, message: t('improve-info.p3') }],
-    lastName: [{ required: true, message: t('improve-info.p3') }],
-    firstName: [{ required: true, message: t('improve-info.p3') }],
-    address: [{ required: true, message: t('improve-info.p3') }],
-    mobile: [{ required: true, message: t('improve-info.p3') }],
-    areaCode: [{ required: true, message: t('improve-info.p5') }],
-}
-const typeOptions = ref([
-    { text: t('kyc.i1'), value: '1' },
-    { text: t('kyc.i2'), value: '2' },
-])
-const infoForm = ref({
-    areaCode: undefined,
-    type: undefined,
-    data: undefined,
-    mobile: undefined,
-    sex: undefined,
-    birthday: undefined,
-    nationality: undefined,
-    town: undefined,
-    postCode: undefined,
-    email: undefined,
-    lastName: undefined,
-    firstName: undefined,
-    address: undefined,
-    cId: undefined,
-})
-const formData = ref<typeof infoForm.value>({ cardTypeId: cardTypeId } as any)
-const formDatas = ref<typeof infoForm.value>({} as any)
-const isShow = ref(false)
-
-// 国家选项
-const countryOptions = ref<Array<{ text: string; value: string }>>([])
-const cityOptions = ref<Array<{ text: string; value: string }>>([])
-const phoneCodes = ref([])
-// 获取国家列表
-const getCountryListForSelect = async () => {
-    try {
-        const res = await ucardApi.ucardCountryCity({})
-        if (res.code === 200 || res.code === 0) {
-            countryOptions.value = res.data.map((item: any) => ({
-                text: lang.value === 'cn' ? item.cnName : item.enName,
-                value: item.code,
-            }))
-        }
-    } catch (error) {
-        countryOptions.value = []
-    }
-}
-// 获取城市列表
-const getCityListForSelect = async (countryCode: string) => {
-    try {
-        const res = await ucardApi.ucardCountryCity({ code: countryCode })
-        if (res.code === 200 || res.code === 0) {
-            const cityList = res.data.map((item: any) => ({
-                text: lang.value === 'cn' ? item.cnName : item.enName,
-                value: item.code,
-            }))
-            cityOptions.value = cityList
-        }
-    } catch (error) {
-        cityOptions.value = []
-    }
-}
-const getCountry = async () => {
-    try {
-        const res = await ucardApi.countryGet()
-        phoneCodes.value = res.data.map((item: { callingCode: string; name: string; enName: string }) => ({
-            text: lang.value === 'cn' ? item.name + ' + ' + item.callingCode : item.enName + ' + ' + item.callingCode,
-            value: item.callingCode,
-        }))
-    } catch (error) {
-        showToast(error || String(error))
-    }
-}
-const handleSubmit = async () => {
-    if (formData.value.type && formData.value.data) {
-        const res = await ucardApi.kycUpload(formData.value as any)
-        if (res.code === 200) {
-            kycSubmit()
-        }
-    }else {
-        showToast(t('kyc.statusDesc5'))
-    }
-}
-const kycSubmit = async () => {
-    const res = await ucardApi.kycSubmit(formData.value as any)
-    if (res.code === 200) {
-        showToast(t('kyc.kycSuccess'))
-        router.push('/cards')
-    }
-}
-const handleChange = (value: any) => {
-    formData.value = { ...formData.value, [value.key]: value.value }
-    if (value.key === 'nationality') {
-        getCityListForSelect(value.value)
-        formData.value.town = ''
-    }
-}
-const containsChinese = (str: string) => /[\u4e00-\u9fa5]/.test(str)
-const convertToPinyin = (value: string) => (containsChinese(value) ? pinyin(value, { toneType: 'none', type: 'capitalize' }) : value)
-const getInfoForm = () => {
-    const customInfo = userInfo.value?.customInfo || {}
-    const fields = [
-        'areaCode',
-        'mobile',
-        'idNo',
-        'sex',
-        'birthday',
-        'nationality',
-        'town',
-        'postCode',
-        'idType',
-        'idNoExpiryDate',
-        'idPicture',
-        'facePicture',
-        'email',
-        'lastName',
-        'firstName',
-        'address',
-        'cId',
-        'uniqueId',
-    ]
-
-    const data: Record<string, any> = {}
-    for (const key of fields) {
-        data[key] = customInfo[key] ?? undefined
-    }
-    if (customInfo.uniqueId) {
-        for (const key of fields) {
-            data[key] = formDatas.value[key] ?? undefined
-        }
-    }
-    data.country = data.nationality
-    data.lastName = convertToPinyin(data.lastName)
-    data.firstName = convertToPinyin(data.firstName)
-    infoForm.value = { ...data }
-    formData.value = { ...formData.value, ...data }
-}
-const getUserInfo = async () => {
-    if (!userToken.value) {
-        showToast('请先登录')
-        return
-    }
-    try {
-        const res = await userApi.getUserSingle()
-        if (res.code === 200 && res.data.uniqueId) {
-            formDatas.value = res.data
-            getInfoForm()
-        }
-    } catch (error: any) {}
-}
-onMounted(async () => {
-    getUserInfo()
-    isShow.value = false
-    await getCountry()
-    await getCountryListForSelect()
-    if (infoForm.value.nationality) {
-        await getCityListForSelect(infoForm.value.nationality)
-        isShow.value = true
-    } else {
-        isShow.value = true
-    }
-})
-</script>
-
-<style scoped lang="scss">
-.page {
-    padding-bottom: 140px;
-}
-.kyc-header {
-    width: 100%;
-    text-align: center;
-    margin-bottom: 24px;
-    h2 {
-        font-size: var(--font-size-18);
-        color: var(--main-yellow);
-        margin-bottom: 8px;
-    }
-    p {
-        color: #bbb;
-        font-size: var(--font-size-12);
-        line-height: 1.5;
-    }
-}
-
-.form-item {
-    margin-bottom: 0;
-    ::v-deep {
-        .van-field {
-            background: #181818;
-            border-radius: 12px;
-            padding: 12px 16px;
-            transition: all 0.3s ease;
-
-            &:focus-within {
-                background: #1f1f1f;
-            }
-
-            .van-field__label {
-                color: #bbb;
-                font-size: var(--font-size-14);
-                width: 80px;
-            }
-
-            .van-field__control {
-                color: #fff;
-                font-size: var(--font-size-14);
-                min-height: 20px;
-                line-height: 20px;
-
-                &::placeholder {
-                    color: #666;
-                    font-size: var(--font-size-14);
-                }
-
-                &::-webkit-input-placeholder {
-                    color: #666;
-                }
-
-                &::-moz-placeholder {
-                    color: #666;
-                    opacity: 1;
-                }
-            }
-
-            .van-field__right-icon {
-                color: var(--main-yellow);
-                margin-left: 8px;
-            }
-
-            .van-field__error-message {
-                color: #ff4d4f;
-                font-size: var(--font-size-12);
-                margin-top: 4px;
-            }
-        }
-    }
-}
-.kyc-button {
-    margin-top: 18px;
-    ::v-deep {
-        .van-button {
-            height: 44px;
-            border-radius: 24px;
-            background: var(--main-yellow);
-            border: none;
-            color: #232323;
-            font-size: var(--font-size-16);
-            font-weight: bold;
-
-            &:active {
-                opacity: 0.9;
-            }
-        }
-    }
-}
-.apply-card-steps {
-    width: 100%;
-    max-width: 350px;
-    margin-bottom: 32px;
-
-    .steps-top {
-        display: flex;
-        align-items: center;
-        justify-content: space-between;
-        padding: 0 12px;
-    }
-
-    .step {
-        display: flex;
-        flex-direction: column;
-        align-items: center;
-        gap: 8px;
-
-        .step-circle {
-            width: 32px;
-            height: 32px;
-            border-radius: 50%;
-            background: var(--action-bg, #181818);
-            border: 2px solid #444;
-            color: #444;
-            display: flex;
-            align-items: center;
-            justify-content: center;
-            font-size: var(--font-size-16);
-            font-weight: bold;
-            transition: all 0.3s ease;
-        }
-
-        .step-label {
-            font-size: var(--font-size-14);
-            color: #444;
-            transition: all 0.3s ease;
-        }
-
-        &.active {
-            .step-circle {
-                background: var(--main-yellow);
-                border-color: var(--main-yellow);
-                color: #232323;
-            }
-
-            .step-label {
-                color: var(--main-yellow);
-            }
-        }
-    }
-
-    .step-dash {
-        flex: 1;
-        height: 2px;
-        margin: 0 8px;
-        position: relative;
-        top: -16px;
-        transition: all 0.3s ease;
-        background-image: linear-gradient(to right, #444 50%, transparent 50%);
-        background-size: 8px 2px;
-        background-repeat: repeat-x;
-
-        &.active {
-            background-image: linear-gradient(to right, var(--main-yellow) 50%, transparent 50%);
-        }
-    }
-}
-
-.phone-input {
-    display: flex;
-    align-items: center;
-    background: #181818;
-    border-radius: 12px;
-    padding: 0 16px;
-    height: 44px;
-    transition: all 0.3s ease;
-
-    &:focus-within {
-        background: #1f1f1f;
-    }
-
-    .phone-code {
-        width: 20%;
-        ::v-deep .van-cell {
-            &::after {
-                border-bottom: 0;
-            }
-            .van-icon {
-                font-size: var(--font-size-14);
-                line-height: 20px;
-            }
-        }
-        ::v-deep {
-            .van-field {
-                background: transparent;
-                padding-left: 0;
-                padding-right: 0;
-
-                .van-field__control {
-                    color: var(--white);
-                    text-align: center;
-                    padding: 0;
-                }
-            }
-        }
-    }
-    .phone-code-active {
-        ::v-deep .van-cell {
-            .van-icon {
-                color: var(--white);
-            }
-        }
-    }
-
-    .phone-divider {
-        width: 1px;
-        height: 20px;
-        background: #333;
-        margin: 0 12px;
-        transition: all 0.3s ease;
-    }
-
-    .phone-number {
-        flex: 1;
-        ::v-deep {
-            .van-field {
-                background: transparent;
-                padding-left: 10px;
-
-                .van-field__control {
-                    color: #fff;
-                    text-align: left;
-                    padding: 0;
-
-                    &::placeholder {
-                        color: #666;
-                        text-align: left;
-                    }
-                }
-            }
-        }
-    }
-}
-
-@keyframes slideDown {
-    from {
-        opacity: 0;
-        transform: translateY(-10px);
-    }
-    to {
-        opacity: 1;
-        transform: translateY(0);
-    }
-}
-
-@keyframes fadeIn {
-    from {
-        opacity: 0;
-    }
-    to {
-        opacity: 0.2;
-    }
-}
-
-:deep(.van-uploader) {
-    width: 100%;
-    .van-uploader__wrapper {
-        width: 100%;
-        display: block;
-    }
-}
-::v-deep(.van-uploader__preview) {
-    width: 100% !important;
-    height: 160px !important;
-    border: 1px dashed #beb6b6;
-    border-radius: 24px;
-    overflow: hidden;
-    .van-uploader__preview-image {
-        width: 100%;
-        height: 100%;
-        .van-image__img {
-            object-fit: contain;
-        }
-    }
-    .van-uploader__preview-delete {
-        position: absolute;
-        top: 0;
-        right: 0;
-        width: 30px;
-        height: 30px;
-        border-radius: 0 24px 0 0;
-        i {
-            text-align: center;
-            line-height: 30px;
-            font-size: 30px;
-        }
-    }
-}
-</style>

+ 3 - 3
src/views/login.vue

@@ -40,11 +40,11 @@
                         <van-checkbox v-model="rememberPassword" shape="square" icon-size="16px"> {{ t('login.p5') }} </van-checkbox>
                         <span class="forgot-password" @click="handleForgotPassword">{{ t('login.p6') }}</span>
                     </div>
-                    <div class="form-options">
+                    <!-- <div class="form-options">
                         <van-checkbox v-model="rememberPassword1" shape="square" icon-size="16px">
                             {{ t('login.p7') }}<span>{{ t('login.p8') }}</span>
                         </van-checkbox>
-                    </div>
+                    </div> -->
 
                     <div class="login-button">
                         <van-button type="primary" block :loading="loading" @click="handleLogin"> {{ t('login.title') }} </van-button>
@@ -130,7 +130,7 @@ const getUserInfo = async () => {
             } else {
                 router.push('/improve/info')
             }
-            showToast(t('login.msg0_1'))
+            // showToast(t('login.msg0_1'))
         } else {
             showToast(res.msg || t('login.msg0'))
         }

+ 2 - 2
src/views/mine.vue

@@ -34,14 +34,14 @@
                 <icon name="cwg-right" :size="23" />
             </div>
         </div>
-        <div class="group">
+        <!-- <div class="group">
             <div class="group-item">
                 <i class="i-mdi-information-outline"></i>
                 <span>{{ t('language.i5') }}</span>
                 <span class="version"></span>
                 <icon name="cwg-right" :size="23" />
             </div>
-        </div>
+        </div> -->
         <div class="group">
             <div class="group-item" @click="showLogoutPopup = true">
                 <i class="i-mdi-logout"></i>

+ 6 - 6
src/views/recharge-record-list.vue

@@ -5,8 +5,8 @@
             <span class="date-str">{{ date }}</span>
             <icon name="cwg-calendar" :size="24" />
         </div>
-        <div class="transactions" v-if="transactions.length > 0">
-            <div v-for="i in transactions" :key="i.id" class="transaction" @click="goToTransactionDetail(i)">
+        <div class="recharge" v-if="recharge.length > 0">
+            <div v-for="i in recharge" :key="i.id" class="transaction" @click="goToTransactionDetail(i)">
                 <div class="trans-icon">
                     <div class="trans-icon-inner">$</div>
                 </div>
@@ -20,7 +20,7 @@
                 </div>
             </div>
         </div>
-        <div class="transactions" v-else>
+        <div class="recharge" v-else>
             <EmptyState :title="t('empty-state.t2')" :text="t('empty-state.c2')" />
         </div>
 
@@ -65,7 +65,7 @@ const statusMap = {
 }
 const route = useRoute()
 const { cardNo } = route.query as { cardNo: string }
-const transactions = ref<Transaction[]>([])
+const recharge = ref<Transaction[]>([])
 
 const showDatePicker = ref(false)
 const dateRange = ref<[string, string] | undefined>(undefined)
@@ -105,7 +105,7 @@ async function getRechargeList() {
             page: { current: 1, row: 10 },
         })
         if (res.code === 200 && res.data) {
-            transactions.value = res.data
+            recharge.value = res.data
         }
     } catch (error) {
         showToast(error as string)
@@ -127,7 +127,7 @@ onMounted(() => {
 </script>
 
 <style scoped lang="scss">
-.transactions {
+.recharge {
     border-radius: 16px;
     margin-bottom: 16px;
     padding: 16px 12px;

+ 2 - 2
src/views/select-card.vue

@@ -84,7 +84,7 @@ const getCardTypes = async () => {
             cards.value = a
         }
     } catch (error) {
-        showToast(error || String(error))
+        showToast(t('common.error'))
     }
 }
 
@@ -253,7 +253,7 @@ onMounted(() => {
 }
 
 :deep(.van-swipe__indicators) {
-    bottom: 390px;
+    top: 220px;
     .van-swipe__indicator {
         width: 9px;
         height: 9px;