zhb 1 rok pred
rodič
commit
6017fdccef

+ 2 - 2
src/assets/scss/global/global.scss

@@ -13,8 +13,8 @@
     --main-bg: #181a1b;
     --card-bg: #222;
     --action-bg: #232323;
-    --main-yellow: #d6ff00;
-    --main-yellow-dark: #b6c800;
+    --main-yellow:  rgb(6 255 139);
+    --main-yellow-dark: rgb(15 120 71);
     --white: #fff;
     --gray: #aaa;
     --border: #333;

+ 7 - 0
src/components.d.ts

@@ -8,6 +8,7 @@ export {}
 /* prettier-ignore */
 declare module 'vue' {
   export interface GlobalComponents {
+    ApplyCard: typeof import('./components/ApplyCard.vue')['default']
     CurrencySelect: typeof import('./components/CurrencySelect.vue')['default']
     CustomTabbar: typeof import('./components/CustomTabbar.vue')['default']
     EmptyComponents: typeof import('./components/empty-components.vue')['default']
@@ -16,6 +17,7 @@ declare module 'vue' {
     PageHeader: typeof import('./components/PageHeader.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    SelectCard: typeof import('./views/select-card.vue')['default']
     ULoadmore: typeof import('./components/u-loadmore.vue')['default']
     VanButton: typeof import('vant/es')['Button']
     VanCell: typeof import('vant/es')['Cell']
@@ -23,6 +25,8 @@ declare module 'vue' {
     VanCheckbox: typeof import('vant/es')['Checkbox']
     VanDatePicker: typeof import('vant/es')['DatePicker']
     VanDialog: typeof import('vant/es')['Dialog']
+    VanDropdownItem: typeof import('vant/es')['DropdownItem']
+    VanDropdownMenu: typeof import('vant/es')['DropdownMenu']
     VanField: typeof import('vant/es')['Field']
     VanGrid: typeof import('vant/es')['Grid']
     VanGridItem: typeof import('vant/es')['GridItem']
@@ -32,9 +36,12 @@ declare module 'vue' {
     VanList: typeof import('vant/es')['List']
     VanLoading: typeof import('vant/es')['Loading']
     VanNavBar: typeof import('vant/es')['NavBar']
+    VanPicker: typeof import('vant/es')['Picker']
     VanPopup: typeof import('vant/es')['Popup']
     VanPullRefresh: typeof import('vant/es')['PullRefresh']
     VanSkeleton: typeof import('vant/es')['Skeleton']
+    VanSwipe: typeof import('vant/es')['Swipe']
+    VanSwipeItem: typeof import('vant/es')['SwipeItem']
     VanTab: typeof import('vant/es')['Tab']
     VanTabbar: typeof import('vant/es')['Tabbar']
     VanTabbarItem: typeof import('vant/es')['TabbarItem']

+ 203 - 0
src/components/ApplyCard.vue

@@ -0,0 +1,203 @@
+<template>
+    <div class="apply-card-content">
+        <div class="apply-card-steps">
+            <div class="steps-top">
+                <div class="step">
+                    <div class="step-circle">1</div>
+                    <div class="step-label">申请购买卡片</div>
+                </div>
+                <div class="step-dash"></div>
+                <div class="step">
+                    <div class="step-circle">2</div>
+                    <div class="step-label">卡片邮寄到家</div>
+                </div>
+                <div class="step-dash"></div>
+                <div class="step">
+                    <div class="step-circle">3</div>
+                    <div class="step-label">激活收到的卡片</div>
+                </div>
+            </div>
+        </div>
+        <button class="apply-btn" @click="handleApply">申请银行卡</button>
+        <button class="activate-btn" @click="handleActivate">已收到卡片? 立即激活</button>
+        <div class="apply-card-footer">
+            <div class="apply-card-empty">
+                <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+                    <rect x="8" y="12" width="32" height="24" rx="4" fill="#232323" stroke="rgb(6 255 139)" stroke-width="2" />
+                    <rect x="20" y="20" width="8" height="2" rx="1" fill="rgb(6 255 139)" />
+                    <circle cx="34" cy="34" r="6" fill="rgb(6 255 139)" />
+                    <rect x="32" y="32" width="4" height="2" rx="1" fill="#232323" />
+                </svg>
+                <div class="empty-text">暂无卡片</div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+
+const handleActivate = () => {
+    router.push('/activate/card')
+}
+
+const handleApply = () => {
+    router.push('/select/card')
+}
+
+</script>
+
+<style scoped>
+.apply-card-header {
+    font-size: 20px;
+    color: #fff;
+    font-weight: 600;
+    margin-bottom: 18px;
+}
+.apply-card-img {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-bottom: 18px;
+}
+.apply-card-visual {
+    position: relative;
+    width: 90px;
+    height: 60px;
+    background: transparent;
+    border-radius: 12px;
+    box-shadow: 0 2px 8px rgba(214, 255, 0, 0.1);
+}
+.chip {
+    position: absolute;
+    right: 12px;
+    top: 22px;
+    width: 22px;
+    height: 16px;
+    background: #eaeaea;
+    border-radius: 4px;
+    border: 1.5px solid #bbb;
+}
+.apply-card-steps {
+    width: 100%;
+    margin-bottom: 52px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+}
+.steps-top {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 100%;
+    position: relative;
+    max-width: 600px;
+    margin: 0 auto;
+}
+.step {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    position: relative;
+    z-index: 2;
+    flex: 0 0 auto;
+}
+.step-circle {
+    width: 28px;
+    height: 28px;
+    background: #232323;
+    color: var(--main-yellow);
+    border-radius: 50%;
+    font-size: 14px;
+    font-weight: bold;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border: 2px solid var(--main-yellow);
+    margin-bottom: 8px;
+    transition: all 0.3s ease;
+}
+.step-circle:hover {
+    transform: scale(1.1);
+    box-shadow: 0 0 10px rgba(73, 247, 166, 0.3);
+}
+.step-dash {
+    flex: 1;
+    height: 2px;
+    background: repeating-linear-gradient(
+        to right,
+        var(--main-yellow),
+        var(--main-yellow) 6px,
+        transparent 6px,
+        transparent 12px
+    );
+    min-width: 18px;
+    position: relative;
+    top: -16px;
+}
+.step-label {
+    color: var(--main-yellow);
+    font-size: 14px;
+    font-weight: 500;
+    text-align: center;
+    line-height: 2;
+}
+.apply-btn {
+    width: 90%;
+    max-width: 320px;
+    background: var(--main-yellow);
+    color: #232323;
+    border: none;
+    border-radius: 24px;
+    font-size: 17px;
+    font-weight: 600;
+    padding: 12px 0;
+    margin: 0 auto 14px;
+    display: block;
+    box-shadow: 0 2px 8px rgba(214, 255, 0, 0.15);
+    cursor: pointer;
+    transition: background 0.2s;
+}
+.apply-btn:hover {
+    background: var(--main-yellow-dark);
+}
+.activate-btn {
+    width: 90%;
+    max-width: 320px;
+    background: transparent;
+    color: var(--main-yellow);
+    border: 1.5px solid var(--main-yellow);
+    border-radius: 24px;
+    font-size: 16px;
+    font-weight: 500;
+    padding: 10px 0;
+    margin: 0 auto 18px;
+    display: block;
+    cursor: pointer;
+    transition: background 0.2s, color 0.2s;
+}
+.activate-btn:hover {
+    background: var(--main-yellow);
+    color: #232323;
+}
+.apply-card-footer {
+    width: 100%;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-top: 48px;
+}
+.apply-card-empty {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 6px;
+}
+.empty-text {
+    color: var(--main-yellow);
+    font-size: 15px;
+    margin-top: 2px;
+}
+</style>

+ 17 - 0
src/composables/useApp.ts

@@ -0,0 +1,17 @@
+import { ref } from 'vue'
+import type { ComponentPublicInstance } from 'vue'
+
+interface AppInstance extends ComponentPublicInstance {
+    showGlobalLoading: () => void
+    hideGlobalLoading: () => void
+}
+
+const appRef = ref<AppInstance | null>(null)
+
+export const useApp = () => {
+    return appRef
+}
+
+export const setAppRef = (ref: AppInstance | null) => {
+    appRef.value = ref
+}

+ 74 - 0
src/locales/cn.ts

@@ -46,6 +46,7 @@ export default {
     i3: '安全设置',
     i4: '购卡记录',
     i5: '关于PayouCard',
+    i6: '退出登录',
   },
   login: {
     title: '登录',
@@ -114,4 +115,77 @@ export default {
     walletPay: '钱包支付',
     rechargeSuccess: '充值成功',
   },
+  "activate-card": {
+    title: '卡片激活',
+    desc: '请输入您的卡片信息进行激活',
+    cardNo: '卡号',
+    cardNoPlaceholder: '请输入卡号',
+    expireDate: '有效期',
+    expireDatePlaceholder: '请输入有效期',
+    cvv: 'CVV',
+    cvvPlaceholder: '请输入CVV',
+    password: '交易密码',
+    passwordPlaceholder: '请输入交易密码',
+    activate: '立即激活',
+  },
+  "select-card": {
+    title: '提交',
+    selectCard: "选择你的卡片",
+    desc: "我们为全球各地用户提供不同的卡片选择,包括实体卡和虚拟卡。",
+    physicalCard: "实体卡",
+    price: "价格:",
+    kycRequired: "KYC认证:",
+    kycRequiredDesc: "需要",
+    kycRequiredDesc2: "不需要",
+    rechargeFee: "充值费用:",
+    atmFee: "ATM取现:",
+    atmFeeDesc: "Free in Europe (other 2%)",
+    atmFeeDesc2: "Free in Europe (other 2%)",
+    spendFee: "消费手续费:",
+    annualFee: "年费:",
+    annualFeeDesc: "Free",
+    usageArea: "使用地区:",
+    free: "免费",
+    submit: "提交",
+    masterCard: "MasterCard 实体卡",
+    visaVirtualCard: "Visa 虚拟卡(至珍卡)",
+    visaPhysicalCard: "Visa 实体卡",
+    visaVirtualCard2: "Visa 虚拟卡(畅游卡)",
+    masterCardDesc: "该卡支持全球线上和线下消费,ATM机取现;支持绑定PayPal/微信/支付宝等场景。",
+    visaVirtualCardDesc: "支持Apple Pay,Google Pay,微信,支付宝场景使用,无单笔消费限额,支持超大额消费。(卡片激活预计1-72小时完成)。",
+    visaPhysicalCardDesc: "该卡片能力强,支持和覆盖全球全平台的线上线下消费,ATM取现等功能(卡片激活时间预计需要48小时)。",
+    visaVirtualCard2Desc: "该卡片支持和覆盖全球全平台的线上消费功能。最擅长商户: Apple,Facebook,Walmart,PAYPAL",
+  },
+  kyc: {
+    title: '身份认证',
+    desc: '请填写您的身份信息以完成认证',
+    firstName: '名',
+    lastName: '姓',
+    address: '邮寄地址',
+    email: '邮箱',
+    phone: '手机号',
+    phoneCode: '区号',
+    postalCode: '邮政编码',
+    submit: '提交',
+    kycSuccess: '身份认证成功',
+    kycFail: '身份认证失败',
+    step1: '身份认证',
+    step2: '支付',
+    step3: '邮寄',
+    step1Desc: '请填写您的身份信息以完成认证',
+    step2Desc: '请填写您的支付信息以完成支付',
+    step3Desc: '请填写您的邮寄信息以完成邮寄',
+    step4Desc: '请填写您的支付信息以完成支付',
+    kycFailDesc: '请填写完整的身份认证信息',
+    emailError: '请输入正确的邮箱格式',
+    phoneError: '请输入正确的手机号格式',
+    postalCodeError: '请输入正确的邮政编码格式',
+    amountError: '请输入正确的金额',
+    currencyError: '请选择正确的币种',
+    shippingAddressError: '请输入正确的邮寄地址',
+    amount: '金额',
+    currency: '币种',
+    shippingAddress: '邮寄地址',
+    next: '下一步',
+  },
 }

+ 2 - 0
src/stores/use-user-store.ts

@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
 import { ref } from 'vue'
 import ls from 'store2'
 import crypt from '../composables/crypt'
+import { userToken } from '../composables/config'
 
 export interface UserInfo {
   id?: string
@@ -32,6 +33,7 @@ const useUserStore = defineStore('userStore', () => {
   const clearUserInfo = () => {
     userInfo.value = null
     isLoggedIn.value = false
+    userToken.value = ''
     ls.remove(STORAGE_KEY)
   }
   initUserInfo()

+ 183 - 0
src/views/activate-card.vue

@@ -0,0 +1,183 @@
+<template>
+    <div class="page">
+        <div class="activate-container">
+            <div class="activate-header">
+                <h2>{{ t('activate-card.title') }}</h2>
+                <p>{{ t('activate-card.desc') }}</p>
+            </div>
+
+            <div class="activate-form">
+                <div class="form-item">
+                    <van-field
+                        v-model="form.cardNo"
+                        :placeholder="t('activate-card.cardNoPlaceholder')"
+                        :rules="[{ required: true, message: t('activate-card.cardNoPlaceholder') }]"
+                    >
+                        <template #left-icon>
+                            <van-icon name="credit-pay" />
+                        </template>
+                    </van-field>
+                </div>
+
+                <div class="form-item">
+                    <van-field
+                        v-model="form.expireDate"
+                        :placeholder="t('activate-card.expireDatePlaceholder')"
+                        :rules="[{ required: true, message: t('activate-card.expireDatePlaceholder') }]"
+                    >
+                        <template #left-icon>
+                            <van-icon name="clock-o" />
+                        </template>
+                    </van-field>
+                </div>
+
+                <div class="form-item">
+                    <van-field
+                        v-model="form.cvv"
+                        :placeholder="t('activate-card.cvvPlaceholder')"
+                        :rules="[{ required: true, message: t('activate-card.cvvPlaceholder') }]"
+                    >
+                        <template #left-icon>
+                            <van-icon name="shield-o" />
+                        </template>
+                    </van-field>
+                </div>
+
+                <div class="form-item">
+                    <van-field
+                        v-model="form.password"
+                        type="password"
+                        :placeholder="t('activate-card.passwordPlaceholder')"
+                        :rules="[
+                            { required: true, message: t('activate-card.passwordPlaceholder') },
+                            { pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,16}$/, message: t('activate-card.passwordPlaceholder') }
+                        ]"
+                    >
+                        <template #left-icon>
+                            <van-icon name="lock" />
+                        </template>
+                    </van-field>
+                </div>
+
+                <div class="activate-button">
+                    <van-button type="primary" block :loading="loading" @click="handleActivate">
+                        {{ t('activate-card.activate') }}
+                    </van-button>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { showToast, showLoadingToast } from 'vant'
+import { useRouter } from 'vue-router'
+import { useI18n } from 'vue-i18n'
+
+const { t } = useI18n()
+
+defineOptions({
+    name: 'ActivateCard',
+})
+
+const router = useRouter()
+const loading = ref(false)
+
+const form = ref({
+    cardNo: '',
+    expireDate: '',
+    cvv: '',
+    password: '',
+})
+
+const handleActivate = async () => {
+    if (!form.value.cardNo || !form.value.expireDate || !form.value.cvv || !form.value.password) {
+        showToast('请填写完整信息')
+        return
+    }
+
+    loading.value = true
+    const loadingToast = showLoadingToast({
+        message: '激活中...',
+        forbidClick: true,
+    })
+
+    try {
+        // TODO: 调用激活API
+        await new Promise(resolve => setTimeout(resolve, 1000))
+        showToast('激活成功')
+        router.push('/cards')
+    } catch (error: any) {
+        showToast(error.message || '激活失败')
+    } finally {
+        loading.value = false
+        loadingToast.close()
+    }
+}
+</script>
+
+<style scoped lang="scss">
+.activate-container {
+    padding: 40px 20px;
+}
+
+.activate-header {
+    text-align: center;
+    margin-bottom: 40px;
+
+    h2 {
+        font-size: var(--font-size-24);
+        color: var(--main-yellow);
+        margin-bottom: 8px;
+    }
+
+    p {
+        font-size: var(--font-size-14);
+        color: var(--gray);
+    }
+}
+
+.activate-form {
+    .form-item {
+        margin-bottom: 20px;
+
+        .van-field {
+            background: var(--action-bg);
+            border-radius: 12px;
+            padding: 12px 16px;
+
+            :deep(.van-field__left-icon) {
+                margin-right: 12px;
+                color: var(--main-yellow);
+            }
+
+            :deep(.van-field__control) {
+                color: var(--white);
+
+                &::placeholder {
+                    color: var(--gray);
+                }
+            }
+        }
+    }
+}
+
+.activate-button {
+    margin-top: 40px;
+
+    .van-button {
+        height: 44px;
+        border-radius: 12px;
+        background: var(--main-yellow);
+        border: none;
+        color: var(--black);
+        font-size: var(--font-size-16);
+        font-weight: bold;
+
+        :deep(&--loading) {
+            opacity: 0.8;
+        }
+    }
+}
+</style>

+ 7 - 7
src/views/card-recharge.vue

@@ -191,7 +191,7 @@ async function estimateRecharge() {
     width: 100%;
     padding: 10px 12px;
     border-radius: 10px;
-    border: 1px solid #d6ff00;
+    border: 1px solid var(--main-yellow);
     background: #181818;
     color: #fff;
     font-size: 16px;
@@ -210,12 +210,12 @@ async function estimateRecharge() {
     justify-content: space-between;
 }
 .balance-value {
-    color: #d6ff00;
+    color: var(--main-yellow);
     font-weight: bold;
     margin: 0 8px;
 }
 .all-btn {
-    color: #d6ff00;
+    color: var(--main-yellow);
     cursor: pointer;
     font-size: 14px;
     margin-left: 8px;
@@ -223,7 +223,7 @@ async function estimateRecharge() {
 .confirm-btn {
     width: 100%;
     height: 44px;
-    background: linear-gradient(90deg, #d6ff00 0%, #eaff7b 100%);
+    background: linear-gradient(90deg, var(--main-yellow) 0%, var(--main-yellow-dark) 100%);
     color: #232323;
     border: none;
     border-radius: 22px;
@@ -231,7 +231,7 @@ async function estimateRecharge() {
     font-weight: bold;
     margin-top: 18px;
     cursor: pointer;
-    box-shadow: 0 2px 12px 0 rgba(214, 255, 0, 0.1);
+    box-shadow: 0 2px 12px 0 rgba(73, 247, 166, 0.1);
     letter-spacing: 2px;
     transition: background 0.2s, box-shadow 0.2s;
 }
@@ -256,7 +256,7 @@ async function estimateRecharge() {
 .trans-title {
     font-size: 16px;
     margin-bottom: 10px;
-    color: var(--main-yellow, #d6ff00);
+    color: var(--main-yellow);
 }
 .transaction {
     display: flex;
@@ -285,7 +285,7 @@ async function estimateRecharge() {
 }
 .trans-amount {
     font-size: 16px;
-    color: var(--main-yellow, #d6ff00);
+    color: var(--main-yellow);
     line-height: 2;
 }
 .trans-date {

+ 94 - 40
src/views/cards.vue

@@ -8,7 +8,12 @@
                     :key="card.id"
                     :style="{ transform: `translateX(${(index - currentIndex) * 100 + offsetX}%)` }"
                 >
-                    <div class="card-info" @click.stop="(e: MouseEvent) => toggleCardNo(card.id, e)" :class="{ flipping: isFlipping[card.id] }">
+                    <div
+                        v-if="card.id !== 'apply'"
+                        class="card-info"
+                        @click.stop="(e: MouseEvent) => toggleCardNo(card.id, e)"
+                        :class="{ flipping: isFlipping[card.id] }"
+                    >
                         <div class="card-front">
                             <div class="owner">
                                 <i class="i-mdi-account-circle-outline" />
@@ -40,47 +45,71 @@
                             </div>
                         </div>
                     </div>
+                    <div class="card-info" v-else>
+                        <div class="apply-card-img">
+                            <div class="apply-card-visual">
+                                <svg width="90" height="60" viewBox="0 0 90 60" fill="none" xmlns="http://www.w3.org/2000/svg">
+                                    <rect x="0" y="0" width="90" height="60" rx="10" fill="#fff" />
+                                    <circle cx="28" cy="22" r="18" fill="url(#paint0_linear)" />
+                                    <defs>
+                                        <linearGradient id="paint0_linear" x1="10" y1="10" x2="46" y2="46" gradientUnits="userSpaceOnUse">
+                                            <stop stop-color="rgb(6 255 139)" />
+                                            <stop offset="1" stop-color="#BFFF00" />
+                                        </linearGradient>
+                                    </defs>
+                                </svg>
+                                <div class="chip"></div>
+                            </div>
+                        </div>
+                    </div>
                 </div>
             </div>
         </div>
-        <div class="actions">
-            <button class="action-btn" @click="goToCardRecharge">
-                <i class="i-mdi-credit-card-plus" />
-                <span>卡片充值</span>
-            </button>
-            <button class="action-btn" @click="goToFindPassword">
-                <i class="i-mdi-lock-reset" />
-                <span>找回密码</span>
-            </button>
-            <button class="action-btn" @click="goToFreezeCard">
-                <i class="i-mdi-credit-card-off" />
-                <span>冻结卡片</span>
-            </button>
-        </div>
-        <div class="balance-wrap">
-            <div class="currency">
-                <img src="https://upload.wikimedia.org/wikipedia/commons/a/a4/Flag_of_the_United_States.svg" class="flag" />
-                <span>USD</span>
+
+        <template v-if="cardList[currentIndex]?.id !== 'apply'">
+            <div class="actions">
+                <button class="action-btn" @click="goToCardRecharge">
+                    <i class="i-mdi-credit-card-plus" />
+                    <span>卡片充值</span>
+                </button>
+                <button class="action-btn" @click="goToFindPassword">
+                    <i class="i-mdi-lock-reset" />
+                    <span>找回密码</span>
+                </button>
+                <button class="action-btn" @click="goToFreezeCard">
+                    <i class="i-mdi-credit-card-off" />
+                    <span>冻结卡片</span>
+                </button>
             </div>
-            <div class="balance">{{ balance.amount }}</div>
-        </div>
-        <div class="transactions">
-            <div class="trans-title">交易记录</div>
-            <div v-for="t in transactions" :key="t.date + t.amount + t.description" class="transaction" @click="goToTransactionDetail(t)">
-                <div class="trans-left">
-                    <div class="trans-type">{{ t.tradeTypeStr }}</div>
-                    <div class="trans-desc">{{ t.remark }}</div>
+            <div class="balance-wrap">
+                <div class="currency">
+                    <img src="https://upload.wikimedia.org/wikipedia/commons/a/a4/Flag_of_the_United_States.svg" class="flag" />
+                    <span>USD</span>
                 </div>
-                <div class="trans-right">
-                    <div class="trans-amount">{{ t.amount }} {{ t.currencyTxn }}</div>
-                    <div class="trans-date">{{ t.businessDate }}</div>
+                <div class="balance">{{ balance.amount }}</div>
+            </div>
+            <div class="transactions">
+                <div class="trans-title">交易记录</div>
+                <div v-for="t in transactions" :key="t.date + t.amount + t.description" class="transaction" @click="goToTransactionDetail(t)">
+                    <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>
+        </template>
+        <div v-else>
+            <ApplyCard />
         </div>
     </div>
 </template>
 
 <script setup lang="ts">
+import ApplyCard from '@/components/ApplyCard.vue'
 const balance = {
     currency: 'USD',
     amount: 340.05,
@@ -94,7 +123,7 @@ const router = useRouter()
 const cardList = ref<CardInfo[]>([])
 const showCardNo = ref<{ [key: string]: boolean }>({})
 const isFlipping = ref<{ [key: string]: boolean }>({})
-const currentIndex = ref(5)
+const currentIndex = ref(6)
 const startX = ref(0)
 const offsetX = ref(0)
 const isDragging = ref(false)
@@ -106,18 +135,41 @@ const getCardList = async () => {
             row: 10,
         },
     })
-    cardList.value = res.data
+    // 手动映射为CardInfo类型
+    cardList.value =
+        res.data && Array.isArray(res.data)
+            ? res.data.map((item: any) => ({
+                  id: item.id,
+                  cardNo: item.cardNo,
+                  firstName: item.firstName || '',
+                  lastName: item.lastName || '',
+                  expire: item.expire || '',
+                  uniqueId: item.uniqueId || '',
+                  ...item,
+              }))
+            : []
     cardList.value.forEach((card) => {
         showCardNo.value[card.id] = false
         isFlipping.value[card.id] = false
     })
-    if (cardList.value.length > 0) {
+    // 添加"申请开卡"虚拟卡片
+    cardList.value.push({
+        id: 'apply',
+        firstName: '',
+        lastName: '',
+        cardNo: '',
+        expire: '',
+        uniqueId: '',
+    })
+    if (cardList.value.length > 1) {
         getTransactions(cardList.value[currentIndex.value].cardNo)
     }
 }
 // 获取交易记录
 const getTransactions = async (cardNo: string) => {
     try {
+        transactions.value = []
+        if (!cardNo) return
         const res = await ucardApi.transactionsList({
             cardNo,
             page: {
@@ -125,9 +177,9 @@ const getTransactions = async (cardNo: string) => {
                 row: 10,
             },
         })
-        transactions.value = res.data || []
-    } catch (error) {
-        showToast(error)
+        transactions.value = res.data && Array.isArray(res.data) ? res.data : []
+    } catch (error: any) {
+        showToast(error?.message || String(error))
         transactions.value = []
     }
 }
@@ -151,17 +203,19 @@ const handleTouchMove = (e: TouchEvent) => {
     const currentX = e.touches[0].clientX
     const diff = currentX - startX.value
     const containerWidth = document.querySelector('.swiper-container')?.clientWidth || 0
+    // 添加弹性效果
     if (currentIndex.value === 0 && diff > 0) {
-        offsetX.value = (diff / containerWidth) * 50
+        offsetX.value = (diff / containerWidth) * 30 // 减小边界阻力
     } else if (currentIndex.value === cardList.value.length - 1 && diff < 0) {
-        offsetX.value = (diff / containerWidth) * 50
+        offsetX.value = (diff / containerWidth) * 30 // 减小边界阻力
     } else {
         offsetX.value = (diff / containerWidth) * 100
     }
 }
 const handleTouchEnd = () => {
     isDragging.value = false
-    if (Math.abs(offsetX.value) > 0.3) {
+    if (Math.abs(offsetX.value) > 0.2) {
+        // 降低切换阈值
         if (offsetX.value > 0 && currentIndex.value > 0) {
             currentIndex.value--
         } else if (offsetX.value < 0 && currentIndex.value < cardList.value.length - 1) {
@@ -228,7 +282,7 @@ function goToTransactionDetail(transaction: TransactionInfo) {
     position: absolute;
     width: 100%;
     height: 100%;
-    transition: transform 0.3s ease;
+    transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); // 优化过渡动画
     will-change: transform;
 }
 

+ 3 - 3
src/views/change-pay-password.vue

@@ -39,7 +39,7 @@ function nextStep() {
         padding: 24px 0 18px 12px;
         .back-icon {
             font-size: 22px;
-            color: var(--main-yellow, #d6ff00);
+            color: var(--main-yellow);
             margin-right: 10px;
             cursor: pointer;
         }
@@ -73,7 +73,7 @@ function nextStep() {
                 color: #fff;
             }
             .code-btn {
-                background: var(--main-yellow, #d6ff00);
+                background: var(--main-yellow);
                 color: #232323;
                 border: none;
                 border-radius: 8px;
@@ -91,7 +91,7 @@ function nextStep() {
         .next-btn {
             width: 100%;
             height: 48px;
-            background: var(--main-yellow, #d6ff00);
+            background: var(--main-yellow);
             color: #232323;
             border: none;
             border-radius: 24px;

+ 5 - 5
src/views/find-password.vue

@@ -115,7 +115,7 @@ async function handleConfirm() {
     justify-content: center;
     margin-bottom: 12px;
     cursor: pointer;
-    border: 2px dashed var(--main-yellow, #d6ff00);
+    border: 2px dashed var(--main-yellow);
     transition: border-color 0.2s;
     overflow: hidden;
     box-shadow: 0 2px 12px 0 rgba(214, 255, 0, 0.04);
@@ -133,7 +133,7 @@ async function handleConfirm() {
 }
 .plus {
     font-size: 48px;
-    color: var(--main-yellow, #d6ff00);
+    color: var(--main-yellow);
     margin-bottom: 0;
     font-weight: bold;
 }
@@ -149,13 +149,13 @@ async function handleConfirm() {
     height: 100%;
     object-fit: cover;
     border-radius: 16px;
-    border: 1.5px solid #d6ff00;
+    border: 1.5px solid var(--main-yellow);
     box-shadow: 0 2px 8px 0 rgba(214, 255, 0, 0.08);
 }
 .confirm-btn {
     width: 100%;
     height: 48px;
-    background: linear-gradient(90deg, #d6ff00 0%, #eaff7b 100%);
+    background: linear-gradient(90deg, var(--main-yellow) 0%, var(--main-yellow-dark) 100%);
     color: #232323;
     border: none;
     border-radius: 24px;
@@ -163,7 +163,7 @@ async function handleConfirm() {
     font-weight: bold;
     margin-top: 30px;
     cursor: pointer;
-    box-shadow: 0 2px 12px 0 rgba(214, 255, 0, 0.1);
+    box-shadow: 0 2px 12px 0 rgba(73, 247, 166, 0.1);
     letter-spacing: 2px;
     transition: background 0.2s, box-shadow 0.2s;
 }

+ 3 - 3
src/views/forget-pay-password.vue

@@ -38,7 +38,7 @@ function nextStep() {
     padding: 24px 0 18px 12px;
     .back-icon {
       font-size: 22px;
-      color: var(--main-yellow, #d6ff00);
+      color: var(--main-yellow);
       margin-right: 10px;
       cursor: pointer;
     }
@@ -72,7 +72,7 @@ function nextStep() {
         color: #fff;
       }
       .code-btn {
-        background: var(--main-yellow, #d6ff00);
+        background: var(--main-yellow);
         color: #232323;
         border: none;
         border-radius: 8px;
@@ -90,7 +90,7 @@ function nextStep() {
     .next-btn {
       width: 100%;
       height: 48px;
-      background: var(--main-yellow, #d6ff00);
+      background: var(--main-yellow);
       color: #232323;
       border: none;
       border-radius: 24px;

+ 7 - 7
src/views/freeze-card.vue

@@ -86,12 +86,12 @@ function handleConfirm() {
     justify-content: center;
     margin-bottom: 12px;
     cursor: pointer;
-    border: 2px dashed var(--main-yellow, #d6ff00);
+    border: 2px dashed var(--main-yellow);
     transition: border-color 0.2s;
     overflow: hidden;
-    box-shadow: 0 2px 12px 0 rgba(214, 255, 0, 0.04);
+    box-shadow: 0 2px 12px 0 rgba(73, 247, 166, 0.04);
     &:hover {
-        border-color: #fff200;
+        border-color: var(--main-yellow);
     }
 }
 .upload-placeholder {
@@ -104,7 +104,7 @@ function handleConfirm() {
 }
 .plus {
     font-size: 48px;
-    color: var(--main-yellow, #d6ff00);
+    color: var(--main-yellow);
     margin-bottom: 0;
     font-weight: bold;
 }
@@ -120,13 +120,13 @@ function handleConfirm() {
     height: 100%;
     object-fit: cover;
     border-radius: 16px;
-    border: 1.5px solid #d6ff00;
+    border: 1.5px solid var(--main-yellow);
     box-shadow: 0 2px 8px 0 rgba(214, 255, 0, 0.08);
 }
 .confirm-btn {
     width: 100%;
     height: 48px;
-    background: linear-gradient(90deg, #d6ff00 0%, #eaff7b 100%);
+    background: linear-gradient(90deg, var(--main-yellow) 0%, var(--main-yellow-dark) 100%);
     color: #232323;
     border: none;
     border-radius: 24px;
@@ -134,7 +134,7 @@ function handleConfirm() {
     font-weight: bold;
     margin-top: 30px;
     cursor: pointer;
-    box-shadow: 0 2px 12px 0 rgba(214, 255, 0, 0.1);
+    box-shadow: 0 2px 12px 0 rgba(73, 247, 166, 0.1);
     letter-spacing: 2px;
     transition: background 0.2s, box-shadow 0.2s;
 }

+ 4 - 4
src/views/home.vue

@@ -66,7 +66,7 @@
         .banner-title {
             font-size: 16px;
             font-weight: bold;
-            color: #d6ff00;
+            color: var(--main-yellow);
             margin-bottom: 6px;
         }
         .banner-desc {
@@ -91,7 +91,7 @@
             background: #111;
         }
         .card-yellow {
-            background: #d6ff00;
+            background: var(--main-yellow);
         }
     }
 }
@@ -126,7 +126,7 @@
             margin-right: 8px;
         }
         .quick-btn {
-            background: #d6ff00;
+            background: var(--main-yellow);
             color: #232323;
             border: none;
             border-radius: 8px;
@@ -150,7 +150,7 @@
             cursor: pointer;
             transition: all 0.2s;
             &:hover {
-                background: #d6ff00;
+                background: var(--main-yellow);
                 color: #232323;
             }
         }

+ 622 - 0
src/views/kyc.vue

@@ -0,0 +1,622 @@
+<template>
+    <div class="page kyc-page">
+        <div class="apply-card-steps">
+            <div class="steps-top">
+                <div class="step" :class="{ active: currentStep >= 1 }">
+                    <div class="step-circle">1</div>
+                    <div class="step-label">{{ t('kyc.step1') }}</div>
+                </div>
+                <div class="step-dash" :class="{ active: currentStep >= 2 }"></div>
+                <div class="step" :class="{ active: currentStep >= 2 }">
+                    <div class="step-circle">2</div>
+                    <div class="step-label">{{ t('kyc.step2') }}</div>
+                </div>
+                <div class="step-dash" :class="{ active: currentStep >= 3 }"></div>
+                <div class="step" :class="{ active: currentStep >= 3 }">
+                    <div class="step-circle">3</div>
+                    <div class="step-label">{{ t('kyc.step3') }}</div>
+                </div>
+            </div>
+        </div>
+        <div class="kyc-form" v-show="currentStep === 1">
+            <div class="form-item">
+                <van-field
+                    v-model="form.firstName"
+                    :placeholder="t('kyc.firstName')"
+                    :rules="[{ required: true, message: t('kyc.firstName') }]"
+                    autocomplete="off"
+                />
+            </div>
+            <div class="form-item">
+                <van-field
+                    v-model="form.lastName"
+                    :placeholder="t('kyc.lastName')"
+                    :rules="[{ required: true, message: t('kyc.lastName') }]"
+                    autocomplete="off"
+                />
+            </div>
+            <div class="form-item">
+                <van-field
+                    v-model="form.address"
+                    :placeholder="t('kyc.address')"
+                    :rules="[{ required: true, message: t('kyc.address') }]"
+                    autocomplete="off"
+                />
+            </div>
+            <div class="form-item">
+                <van-field
+                    v-model="form.email"
+                    :placeholder="t('kyc.email')"
+                    :rules="[{ required: true, message: t('kyc.email') }]"
+                    autocomplete="off"
+                />
+            </div>
+            <div class="form-item phone-input">
+                <div :class="{ 'phone-code': true, 'phone-code-active': form.phoneCode }">
+                    <van-field
+                        is-link
+                        readonly
+                        @click="showPicker = true"
+                        v-model="form.phoneCode"
+                        :placeholder="t('kyc.phoneCode')"
+                        :rules="[{ required: true, message: t('kyc.phoneCode') }]"
+                        autocomplete="off"
+                    />
+                    <van-popup v-model:show="showPicker" destroy-on-close round position="bottom">
+                        <van-picker
+                            v-model="form.phoneCode1"
+                            :visible-option-num="8"
+                            show-toolbar
+                            title="选择区号"
+                            :columns="[phoneCodes]"
+                            @cancel="showPicker = false"
+                            @confirm="onConfirm"
+                        />
+                    </van-popup>
+                </div>
+                <div class="phone-divider"></div>
+                <div class="phone-number">
+                    <van-field
+                        v-model="form.phone"
+                        :placeholder="t('kyc.phone')"
+                        :rules="[{ required: true, message: t('kyc.phone') }]"
+                        autocomplete="off"
+                    />
+                </div>
+            </div>
+            <div class="kyc-button">
+                <van-button type="primary" block @click="handleNext">{{ t('kyc.next') }}</van-button>
+            </div>
+        </div>
+        <div class="kyc-form" v-show="currentStep === 2">
+            <div class="form-item">
+                <van-field
+                    v-model="form.amount"
+                    type="number"
+                    :placeholder="t('kyc.amount')"
+                    :rules="[{ required: true, message: t('kyc.amount') }]"
+                    autocomplete="off"
+                />
+            </div>
+            <div class="form-item">
+                <van-field
+                    v-model="form.currency"
+                    :placeholder="t('kyc.currency')"
+                    :rules="[{ required: true, message: t('kyc.currency') }]"
+                    autocomplete="off"
+                />
+            </div>
+            <div class="kyc-button">
+                <van-button type="primary" block @click="handleNext">{{ t('kyc.next') }}</van-button>
+            </div>
+        </div>
+        <div class="kyc-form" v-show="currentStep === 3">
+            <div class="form-item">
+                <van-field
+                    v-model="form.shippingAddress"
+                    :placeholder="t('kyc.shippingAddress')"
+                    :rules="[{ required: true, message: t('kyc.shippingAddress') }]"
+                    autocomplete="off"
+                />
+            </div>
+            <div class="form-item">
+                <van-field
+                    v-model="form.postalCode"
+                    :placeholder="t('kyc.postalCode')"
+                    :rules="[{ required: true, message: t('kyc.postalCode') }]"
+                    autocomplete="off"
+                />
+            </div>
+            <div class="kyc-button">
+                <van-button type="primary" block @click="handleSubmit">{{ t('kyc.submit') }}</van-button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { useI18n } from 'vue-i18n'
+import { showToast } from 'vant'
+
+const { t } = useI18n()
+const router = useRouter()
+const currentStep = ref(1)
+
+const showPicker = ref(false)
+const onConfirm = (value: any) => {
+    form.value.phoneCode = value.selectedValues[0]
+    showPicker.value = false
+}
+
+const form = ref({
+    // 第一步
+    firstName: '',
+    lastName: '',
+    address: '',
+    email: '',
+    phone: '',
+    phoneCode: '',
+    phoneCode1: [],
+    // 第二步
+    amount: '',
+    currency: '',
+    // 第三步
+    shippingAddress: '',
+    postalCode: '',
+})
+
+// 区号选项
+const phoneCodes = [
+    { text: '中国 +86', value: '+86' },
+    { text: '美国 +1', value: '+1' },
+    { text: '英国 +44', value: '+44' },
+    { text: '日本 +81', value: '+81' },
+    { text: '韩国 +82', value: '+82' },
+    { text: '新加坡 +65', value: '+65' },
+    { text: '澳大利亚 +61', value: '+61' },
+    { text: '香港 +852', value: '+852' },
+    { text: '澳门 +853', value: '+853' },
+    { text: '台湾 +886', value: '+886' },
+    { text: '柬埔寨 +855', value: '+855' },
+    { text: '老挝 +856', value: '+856' },
+    { text: '缅甸 +95', value: '+95' },
+    { text: '泰国 +66', value: '+66' },
+    { text: '越南 +84', value: '+84' },
+    { text: '马来西亚 +60', value: '+60' },
+    { text: '菲律宾 +63', value: '+63' },
+    { text: '印度尼西亚 +62', value: '+62' },
+    { text: '印度 +91', value: '+91' },
+    { text: '巴基斯坦 +92', value: '+92' },
+]
+
+// 验证规则
+const validations = {
+    email: (value: string) => {
+        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+        return emailRegex.test(value)
+    },
+    phone: (value: string) => {
+        const phoneRegex = /^1[3-9]\d{9}$/
+        return phoneRegex.test(value)
+    },
+    amount: (value: string) => {
+        const num = Number(value)
+        return !isNaN(num) && num > 0
+    },
+    postalCode: (value: string) => {
+        const postalCodeRegex = /^\d{6}$/
+        return postalCodeRegex.test(value)
+    },
+}
+
+function validateStep(step: number): boolean {
+    switch (step) {
+        case 1:
+            if (!form.value.firstName || !form.value.lastName || !form.value.address || !form.value.email || !form.value.phone) {
+                showToast(t('kyc.kycFailDesc'))
+                return false
+            }
+            if (!validations.email(form.value.email)) {
+                showToast(t('kyc.emailError'))
+                return false
+            }
+            if (!validations.phone(form.value.phone)) {
+                showToast(t('kyc.phoneError'))
+                return false
+            }
+            break
+        case 2:
+            if (!form.value.amount || !form.value.currency) {
+                showToast(t('kyc.kycFailDesc'))
+                return false
+            }
+            if (!validations.amount(form.value.amount)) {
+                showToast(t('kyc.amountError'))
+                return false
+            }
+            break
+        case 3:
+            if (!form.value.shippingAddress || !form.value.postalCode) {
+                showToast(t('kyc.kycFailDesc'))
+                return false
+            }
+            if (!validations.postalCode(form.value.postalCode)) {
+                showToast(t('kyc.postalCodeError'))
+                return false
+            }
+            break
+    }
+    return true
+}
+
+function handleNext() {
+    if (validateStep(currentStep.value)) {
+        if (currentStep.value < 3) {
+            currentStep.value++
+        }
+    }
+}
+
+function handleSubmit() {
+    // 验证所有步骤
+    for (let step = 1; step <= 3; step++) {
+        if (!validateStep(step)) {
+            return
+        }
+    }
+
+    // 提交所有表单数据
+    console.log('提交表单数据:', form.value)
+    router.push('/kyc/success')
+}
+</script>
+
+<style scoped lang="scss">
+.kyc-page {
+    min-height: 100vh;
+    background: linear-gradient(135deg, #232323 0%, #2a2a2a 100%);
+    color: #fff;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 32px 12px;
+}
+.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;
+    }
+}
+.kyc-form {
+    width: 100%;
+    max-width: 350px;
+    border-radius: 20px;
+    padding: 28px 18px 18px 18px;
+    display: flex;
+    flex-direction: column;
+    gap: 18px;
+}
+
+.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;
+    }
+}
+
+::v-deep {
+    .van-picker {
+        background: #181818;
+
+        .van-picker__toolbar {
+            background: #181818;
+            border-bottom: 1px solid #333;
+            height: 44px;
+            padding: 0 16px;
+
+            .van-picker__title {
+                color: #fff;
+                font-size: var(--font-size-16);
+                font-weight: 500;
+            }
+
+            .van-picker__cancel,
+            .van-picker__confirm {
+                color: var(--main-yellow);
+                font-size: var(--font-size-14);
+                padding: 0 8px;
+                height: 28px;
+                line-height: 28px;
+                border-radius: 14px;
+                transition: all 0.3s ease;
+
+                &:active {
+                    opacity: 0.8;
+                    background: rgba(255, 193, 7, 0.1);
+                }
+            }
+        }
+
+        .van-picker-column {
+            color: #fff;
+
+            .van-picker-column__item {
+                color: #fff;
+                font-size: var(--font-size-14);
+                padding: 0 16px;
+                height: 44px;
+                line-height: 44px;
+                transition: all 0.3s ease;
+
+                &--selected {
+                    color: var(--main-yellow);
+                    font-weight: 500;
+                    font-size: var(--font-size-16);
+                }
+
+                &:active {
+                    background: rgba(255, 255, 255, 0.05);
+                }
+            }
+
+            .van-picker-column__wrapper {
+                &::after {
+                    border-color: #333;
+                }
+            }
+        }
+
+        .van-picker__mask {
+            background-image: linear-gradient(180deg, rgba(24, 24, 24, 0.9), rgba(24, 24, 24, 0.4)),
+                linear-gradient(0deg, rgba(24, 24, 24, 0.9), rgba(24, 24, 24, 0.4));
+        }
+
+        .van-picker__indicator {
+            height: 44px;
+            background: rgba(255, 193, 7, 0.05);
+            border-top: 1px solid rgba(255, 193, 7, 0.1);
+            border-bottom: 1px solid rgba(255, 193, 7, 0.1);
+        }
+    }
+}
+</style>

+ 19 - 6
src/views/mine.vue

@@ -9,40 +9,52 @@
         </div>
         <div class="group">
             <div class="group-item" @click="$router.push('/language')">
-                <van-icon name="setting-o" />
+                <i class="i-mdi-web"></i>
                 <span>{{ t('language.i1') }}</span>
             </div>
             <div class="group-item">
-                <van-icon name="notes-o" />
+                <i class="i-mdi-history"></i>
                 <span>{{ t('language.i4') }}</span>
             </div>
         </div>
         <div class="group">
             <div class="group-item" @click="$router.push('/pay/password')">
-                <van-icon name="lock" />
+                <i class="i-mdi-lock-reset"></i>
                 <span>{{ t('language.i2') }}</span>
             </div>
             <div class="group-item">
-                <van-icon name="shield-o" />
+                <i class="i-mdi-shield-lock-outline"></i>
                 <span>{{ t('language.i3') }}</span>
             </div>
         </div>
         <div class="group">
             <div class="group-item">
-                <van-icon name="info-o" />
+                <i class="i-mdi-information-outline"></i>
                 <span>{{ t('language.i5') }}</span>
                 <span class="version">v2.0.21</span>
             </div>
         </div>
+        <div class="group">
+            <div class="group-item" @click="handleLogout">
+                <i class="i-mdi-logout"></i>
+                <span>{{ t('language.i6') }}</span>
+            </div>
+        </div>
     </div>
 </template>
 
 <script setup lang="ts">
 import { useI18n } from 'vue-i18n'
 const { t } = useI18n()
+import { useRouter } from 'vue-router'
+const router = useRouter()
 import useUserStore from '@/stores/use-user-store'
 const userStore = useUserStore()
 const userInfo = userStore.userInfo
+const handleLogout = () => {
+    userStore.clearUserInfo()
+    router.push('/login')
+}
 </script>
 
 <style scoped>
@@ -93,8 +105,9 @@ const userInfo = userStore.userInfo
 .group-item:last-child {
     border-bottom: none;
 }
-.van-icon{
+i{
     color: var(--main-yellow);
+    font-size: var(--font-size-18);
     margin-right: 12px;
 }
 .group-item .version {

+ 1 - 1
src/views/pay-password.vue

@@ -40,7 +40,7 @@
     padding: 24px 0 18px 12px;
     .back-icon {
       font-size: 22px;
-      color: var(--main-yellow, #d6ff00);
+      color: var(--main-yellow);
       margin-right: 10px;
       cursor: pointer;
     }

+ 330 - 0
src/views/select-card.vue

@@ -0,0 +1,330 @@
+<template>
+    <div class="select-card-content">
+        <div class="select-card-header">{{ t('select-card.selectCard') }}</div>
+        <div class="select-card-desc">{{ t('select-card.desc') }}</div>
+        <div class="cards-container">
+            <van-swipe class="cards-wrapper" :show-indicators="false" :loop="false" :duration="300">
+                <van-swipe-item v-for="(card, index) in cards" :key="index" class="card-box">
+                    <div class="card-visual">
+                        <div class="card-chip"></div>
+                    </div>
+                    <div class="card-info">
+                        <div class="card-title">{{ card.title }}</div>
+                        <div class="card-content">{{ card.content }}</div>
+                        <ul class="card-list">
+                            <li v-for="(item, key) in card.details" :key="key">
+                                <span>{{ t(`select-card.${key}`) }}</span>
+                                <span>{{ item }}</span>
+                            </li>
+                        </ul>
+                    </div>
+                </van-swipe-item>
+            </van-swipe>
+        </div>
+        <button class="submit-btn" @click="handleSubmit">{{ t('select-card.submit') }}</button>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { useRouter } from 'vue-router'
+import { useI18n } from 'vue-i18n'
+
+const router = useRouter()
+const { t } = useI18n()
+
+const cards = [
+    {
+        title: t('select-card.masterCard'),
+        content: t('select-card.masterCardDesc'),
+        details: {
+            price: '99 USDT',
+            kycRequired: t('select-card.kycRequiredDesc'),
+            rechargeFee: '1.8%',
+            atmFee: t('select-card.atmFeeDesc'),
+            spendFee: t('select-card.spendFeeDesc'),
+            annualFee: '12 EUR',
+            usageArea: 'Worldwide',
+        },
+    },
+    {
+        title: t('select-card.visaVirtualCard'),
+        content: t('select-card.visaVirtualCardDesc'),
+        details: {
+            price: '29 USDT',
+            kycRequired: t('select-card.kycRequiredDesc2'),
+            rechargeFee: '2%',
+            atmFee: '0%',
+            spendFee: '0%',
+            annualFee: t('select-card.annualFeeDesc'),
+            usageArea: 'Worldwide',
+        },
+    },
+    {
+        title: t('select-card.visaPhysicalCard'),
+        content: t('select-card.visaPhysicalCardDesc'),
+        details: {
+            price: '239 USDT',
+            kycRequired: t('select-card.kycRequiredDesc'),
+            rechargeFee: '2.99%',
+            atmFee: t('select-card.atmFeeDesc'),
+            spendFee: t('select-card.spendFeeDesc'),
+            annualFee: t('select-card.annualFeeDesc'),
+            usageArea: 'Worldwide',
+        },
+    },
+    {
+        title: t('select-card.visaVirtualCard2'),
+        content: t('select-card.visaVirtualCard2Desc'),
+        details: {
+            price: '10 USDT',
+            kycRequired: t('select-card.kycRequiredDesc2'),
+            rechargeFee: '2%',
+            atmFee: '2%',
+            spendFee: '2%',
+            annualFee: t('select-card.annualFeeDesc'),
+            usageArea: 'Worldwide',
+        },
+    },
+]
+
+const handleSubmit = () => {
+    router.push('/kyc')
+}
+</script>
+
+<style scoped lang="scss">
+::v-deep {
+    .van-swipe__track {
+        width: 100% !important;
+        background: linear-gradient(145deg, #1a1a1a, #181818);
+    }
+}
+.select-card-content {
+    min-height: 100vh;
+    background: #111;
+    color: #fff;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 32px 12px;
+}
+
+.select-card-header {
+    font-size: var(--font-size-18);
+    color: var(--main-yellow);
+    margin-bottom: 12px;
+    text-align: center;
+}
+
+.select-card-desc {
+    color: #bbb;
+    width: 100%;
+    text-align: center;
+    font-size: var(--font-size-12);
+    margin-bottom: 32px;
+    text-align: center;
+    line-height: 1.5;
+}
+
+.cards-container {
+    width: 100%;
+    max-width: 350px;
+    position: relative;
+    overflow: hidden;
+    margin-bottom: 40px;
+    height: 500px;
+}
+
+.cards-wrapper {
+    display: flex;
+    transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+    touch-action: pan-y pinch-zoom;
+    will-change: transform;
+    justify-content: center;
+    gap: 0px;
+    height: 100%;
+}
+
+.card-box {
+    flex: 0 0 80%;
+    background: linear-gradient(145deg, #1a1a1a, #181818);
+    border: 2px solid var(--main-yellow);
+    border-radius: 20px;
+    padding: 28px 14px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    box-shadow: 0 4px 20px rgba(73, 247, 166, 0.1);
+    margin: 0 10%;
+    width: 80% !important;
+    transform-style: preserve-3d;
+    transition: all 0.3s ease;
+    height: 100%;
+    box-sizing: border-box;
+}
+
+.card-box:active {
+    transform: scale(0.98);
+}
+
+.card-visual {
+    width: 70px;
+    height: 45px;
+    background: linear-gradient(145deg, #2a2a2a, #232323);
+    border-radius: 10px;
+    margin-bottom: 20px;
+    position: relative;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+    transition: transform 0.3s ease;
+}
+
+.card-visual:hover {
+    transform: translateY(-2px);
+}
+
+.card-info {
+    width: 100%;
+}
+
+.card-chip {
+    position: absolute;
+    right: 12px;
+    top: 16px;
+    width: 18px;
+    height: 12px;
+    background: linear-gradient(145deg, #f5f5f5, #eaeaea);
+    border-radius: 3px;
+    border: 1px solid #bbb;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.card-title {
+    font-size: var(--font-size-16);
+    color: #fff;
+    margin-bottom: 10px;
+    text-align: left;
+    line-height: 1.4;
+}
+
+.card-list {
+    list-style: none;
+    padding: 0;
+    margin: 0;
+    width: 100%;
+    flex: 1;
+    overflow-y: auto;
+}
+
+.card-list li {
+    display: flex;
+    justify-content: space-between;
+    font-size: var(--font-size-12);
+    color: var(--main-yellow);
+    margin-bottom: 12px;
+    padding: 8px 0;
+    border-bottom: 1px solid rgba(73, 247, 166, 0.1);
+    transition: all 0.3s ease;
+    position: relative;
+    padding-left: 24px;
+}
+
+.card-list li::before {
+    content: '✓';
+    position: absolute;
+    left: 0;
+    top: 50%;
+    width: 16px;
+    height: 16px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 50%;
+    transform: translateY(-50%);
+    color: #000;
+    background: var(--main-yellow);
+    font-size: 10px;
+    font-weight: bold;
+}
+
+.card-list li:hover {
+    background: rgba(73, 247, 166, 0.1);
+    border-radius: 6px;
+}
+
+.card-list li:last-child {
+    border-bottom: none;
+    margin-bottom: 0;
+}
+
+.card-list li span:first-child {
+    color: #bbb;
+}
+
+.card-dots {
+    display: flex;
+    justify-content: center;
+    gap: 10px;
+    margin-top: 20px;
+}
+
+.card-content {
+    font-size: var(--font-size-10);
+    color: #bbb;
+    margin-bottom: 26px;
+    text-align: left;
+    line-height: 1.5;
+}
+
+.dot {
+    width: 8px;
+    height: 8px;
+    border-radius: 50%;
+    background: #333;
+    cursor: pointer;
+    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    position: relative;
+}
+
+.dot.active {
+    background: var(--main-yellow);
+    transform: scale(1.1);
+    box-shadow: 0 0 8px rgba(73, 247, 166, 0.4);
+}
+
+.dot:hover {
+    transform: scale(1.1);
+    background: #444;
+}
+
+.dot.active:hover {
+    background: #eaff6b;
+}
+
+.submit-btn {
+    width: 90%;
+    max-width: 320px;
+    background: var(--main-yellow);
+    color: #232323;
+    border: none;
+    border-radius: 24px;
+    font-size: 17px;
+    font-weight: 600;
+    padding: 14px 0;
+    margin: 0 auto 14px;
+    display: block;
+    box-shadow: 0 4px 12px rgba(73, 247, 166, 0.2);
+    cursor: pointer;
+    transition: all 0.3s;
+}
+
+.submit-btn:hover {
+    background: var(--main-yellow-dark);
+    transform: translateY(-2px);
+    box-shadow: 0 6px 16px rgba(73, 247, 166, 0.3);
+}
+
+.submit-btn:active {
+    transform: translateY(0);
+    box-shadow: 0 2px 8px rgba(73, 247, 166, 0.2);
+}
+</style>