Parcourir la source

加密货币充值、提现

zhb il y a 4 mois
Parent
commit
a766d312d1
44 fichiers modifiés avec 3535 ajouts et 613 suppressions
  1. 26 2
      api/ucard.ts
  2. 7 6
      components/cwg-input.vue
  3. 3 3
      config/index.ts
  4. 577 135
      hooks/useAppUpdate.ts
  5. 6 1
      locale/cn.json
  6. 3 2
      manifest.json
  7. 2 1
      package.json
  8. 35 0
      pages.json
  9. 0 3
      pages/card/components/FirstApply.vue
  10. 12 0
      pages/mine/index.vue
  11. 14 68
      pages/recharge-record/components/DeductionList.vue
  12. 16 32
      pages/recharge-record/components/RechargeList.vue
  13. 14 24
      pages/recharge-record/components/TransactionList.vue
  14. 0 214
      pages/recharge-record/detail copy.vue
  15. 57 84
      pages/recharge-record/detail.vue
  16. 2 23
      pages/wallet/components/GlobalList.vue
  17. 482 0
      pages/wallet/components/VaultodyList.vue
  18. 477 0
      pages/wallet/components/WithdrawList.vue
  19. 12 7
      pages/wallet/global-detail.vue
  20. 30 5
      pages/wallet/index.vue
  21. 277 0
      pages/wallet/vaultody-list.vue
  22. 185 0
      pages/wallet/vaultody.vue
  23. 659 0
      pages/wallet/withdraw-detail.vue
  24. 277 0
      pages/wallet/withdraw-list.vue
  25. 339 0
      pages/wallet/withdraw.vue
  26. BIN
      unpackage/res/icons/1024x1024.png
  27. BIN
      unpackage/res/icons/120x120.png
  28. BIN
      unpackage/res/icons/144x144.png
  29. BIN
      unpackage/res/icons/152x152.png
  30. BIN
      unpackage/res/icons/167x167.png
  31. BIN
      unpackage/res/icons/180x180.png
  32. BIN
      unpackage/res/icons/192x192.png
  33. BIN
      unpackage/res/icons/20x20.png
  34. BIN
      unpackage/res/icons/29x29.png
  35. BIN
      unpackage/res/icons/40x40.png
  36. BIN
      unpackage/res/icons/58x58.png
  37. BIN
      unpackage/res/icons/60x60.png
  38. BIN
      unpackage/res/icons/72x72.png
  39. BIN
      unpackage/res/icons/76x76.png
  40. BIN
      unpackage/res/icons/80x80.png
  41. BIN
      unpackage/res/icons/87x87.png
  42. BIN
      unpackage/res/icons/96x96.png
  43. 20 0
      utils/dataMap.js
  44. 3 3
      utils/request.js

+ 26 - 2
api/ucard.ts

@@ -575,14 +575,38 @@ export const ucardApi = {
   getVaultodyAddress(
     params: object = {}
   ): Promise<BaseResponse<any>> {
-    return post("/vaultody/generate/deposit/address", params);
+    return post("/wasabi/api/card/wallet/generate/deposit/address", params);
   },
 
   // 加密货币交易记录列表
   getBlockchainTransactionPage(
     params: object = {}
   ): Promise<BaseResponse<any>> {
-    return post("/wasabi/api/encrypted/wallet/transaction/page", params);
+    return post("/wasabi/api/card/wallet/deposit/page", params);
+  },
+  // 新增钱包提款列表
+  getBlockchainWithdrawPage(
+    params: object = {}
+  ): Promise<BaseResponse<any>> {
+    return post("/wasabi/api/card/wallet/withdraw/page", params);
+  },
+  // 取消
+  getBlockchainWithdrawCancel(
+    params: object = {}
+  ): Promise<BaseResponse<any>> {
+    return post("/wasabi/api/card/wallet/withdraw/cancel", params);
+  },
+  // 新建钱包提款发送邮箱验证码 
+  getBlockchainWithdrawSendEmailCode(
+    params: object = {}
+  ): Promise<BaseResponse<any>> {
+    return post("/wasabi/api/card/wallet/send/email/code", params);
+  },
+  // 新增钱包提款
+  getBlockchainWithdrawApply(
+    params: object = {}
+  ): Promise<BaseResponse<any>> {
+    return post("/wasabi/api/card/wallet/withdraw/apply", params);
   },
 
   // 卡扣款分页列表

+ 7 - 6
components/cwg-input.vue

@@ -410,15 +410,12 @@ function onDateConfirm(event) {
 
 .form-label {
   font-size: var(--font-size-16);
-  line-height: px2rpx(44);
+  line-height: px2rpx(24);
   letter-spacing: px2rpx(1);
   color: #474747;
-  margin-bottom: px2rpx(4);
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
+  margin: px2rpx(16) 0 px2rpx(8) 0;
   display: flex;
-  align-items: center;
+  align-self: start;
 
   .required-mark {
     color: red;
@@ -488,6 +485,10 @@ function onDateConfirm(event) {
 :deep(.up-icon) {
   color: var(--black1) !important;
 }
+:deep(.u-form-item__body__right__message) {
+  position: relative;
+  top: px2rpx(4);
+}
 
 :deep(.u-upload__wrap) {
   width: 100%;

+ 3 - 3
config/index.ts

@@ -1,9 +1,9 @@
 const config = {
   // Host00: 'https://secure.cwgrd.com',
   // Host85: 'https://ucard.cwgrd.com',
-  // Host85: 'http://192.168.0.25:8700',
-  Host00: "https://ucard.44a5c8109e4.com",
-  Host85: "https://ucard.44a5c8109e4.com",
+  Host85: 'http://192.168.0.25:8700',
+  // Host00: "https://ucard.44a5c8109e4.com",
+  // Host85: "https://ucard.44a5c8109e4.com",
   
   Code: {
     StatusOK: 200,

+ 577 - 135
hooks/useAppUpdate.ts

@@ -4,9 +4,208 @@ import { useProgress } from './useProgress'
 import useUserStore from '@/stores/use-user-store'
 import { ucardApi } from '@/api/ucard'
 
-const LAST_CHECK_KEY = 'last_update_check_time'
-const SKIP_VERSION_KEY = 'skip_update_version'
-const DOWNLOAD_CACHE_KEY = 'app_download_cache'
+// ================== 类型声明 ==================
+
+declare const plus: any
+declare const uni: any
+
+interface PlusDownloaderDownload {
+    filename: string
+    pause?: () => void
+    resume?: () => void
+    start: () => void
+    abort: () => void
+    addEventListener: (event: string, callback: (data: any) => void) => void
+    removeEventListener?: (event: string, callback: (data: any) => void) => void
+}
+
+// ================== 常量定义 ==================
+
+/** 存储键名 */
+const STORAGE_KEYS = {
+    LAST_CHECK: 'last_update_check_time',
+    SKIP_VERSION: 'skip_update_version',
+    DOWNLOAD_CACHE: 'app_download_cache'
+} as const
+
+/** 下载状态码 */
+enum DownloadState {
+    DOWNLOADING = 3, // 下载中
+    COMPLETED = 4    // 下载完成
+}
+
+/** HTTP 成功状态码 */
+enum HttpStatus {
+    OK = 200,            // 成功
+    PARTIAL_CONTENT = 206 // 部分内容(断点续传)
+}
+
+/** 下载配置 */
+const DOWNLOAD_CONFIG = {
+    FILE_PATH: '_downloads/app_update.wgt',
+    MAX_RETRY: 3,              // 最大重试次数
+    RETRY_DELAY: 2000,         // 重试延迟(毫秒)
+    TOAST_DURATION: 2000       // Toast 显示时长
+} as const
+
+// ================== 类型定义 ==================
+
+/** 更新信息 */
+interface UpdateInfo {
+    version: string
+    forceUpdate: boolean
+    wgtUrl?: string
+    iosStoreUrl?: string
+}
+
+/** 下载缓存 */
+interface DownloadCache {
+    url: string
+    progress: number
+    time: number
+}
+
+/** API 响应 */
+interface ApiResponse<T = any> {
+    code: number
+    data?: T
+    message?: string
+}
+
+/** 下载状态变化事件 */
+interface DownloadStateEvent {
+    state: number
+    downloadedSize: number
+    totalSize: number
+}
+
+/** 平台类型 */
+type PlatformType = 'ios' | 'android'
+
+/** 设备类型 */
+type EquipmentType = 'ios' | 'Android'
+
+// ================== 工具函数 ==================
+
+/**
+ * 获取当前应用版本
+ */
+function getCurrentVersion(): Promise<string> {
+    return new Promise((resolve, reject) => {
+        // #ifdef APP-PLUS
+        try {
+            plus.runtime.getProperty(plus.runtime.appid, (info) => {
+                resolve(info.version)
+            }, (error) => {
+                reject(error)
+            })
+        } catch (error) {
+            reject(error)
+        }
+        // #endif
+        // #ifndef APP-PLUS
+        reject(new Error('Not in APP-PLUS environment'))
+        // #endif
+    })
+}
+
+/**
+ * 比较版本号
+ * @returns 1: v1 > v2, -1: v1 < v2, 0: v1 === v2
+ */
+function compareVersion(v1: string, v2: string): number {
+    const normalizeVersion = (v: string): number[] => {
+        return v.split('.').map(segment => parseInt(segment || '0', 10))
+    }
+
+    const parts1 = normalizeVersion(v1)
+    const parts2 = normalizeVersion(v2)
+    const maxLength = Math.max(parts1.length, parts2.length)
+
+    for (let i = 0; i < maxLength; i++) {
+        const num1 = parts1[i] || 0
+        const num2 = parts2[i] || 0
+        if (num1 > num2) return 1
+        if (num1 < num2) return -1
+    }
+    return 0
+}
+
+/**
+ * 获取平台类型
+ */
+function getPlatform(): PlatformType {
+    // #ifdef APP-PLUS
+    try {
+        const platform = uni.getSystemInfoSync().platform
+        return platform === 'ios' ? 'ios' : 'android'
+    } catch (error) {
+        console.error('获取平台类型失败:', error)
+        return 'android'
+    }
+    // #endif
+    // #ifndef APP-PLUS
+    return 'android'
+    // #endif
+}
+
+/**
+ * 获取设备类型(API 参数格式)
+ */
+function getEquipmentType(): EquipmentType {
+    return getPlatform() === 'ios' ? 'ios' : 'Android'
+}
+
+/**
+ * 显示 Toast 提示
+ */
+function showToast(message: string, duration = DOWNLOAD_CONFIG.TOAST_DURATION): void {
+    uni.showToast({
+        title: message,
+        icon: 'none',
+        duration
+    })
+}
+
+/**
+ * 安全地获取存储值
+ */
+function getStorageSync<T = any>(key: string, defaultValue: T | null = null): T | null {
+    try {
+        return uni.getStorageSync(key) || defaultValue
+    } catch (error) {
+        console.warn(`获取存储失败: ${key}`, error)
+        return defaultValue
+    }
+}
+
+/**
+ * 安全地设置存储值
+ */
+function setStorageSync(key: string, value: any): boolean {
+    try {
+        uni.setStorageSync(key, value)
+        return true
+    } catch (error) {
+        console.warn(`设置存储失败: ${key}`, error)
+        return false
+    }
+}
+
+/**
+ * 安全地移除存储值
+ */
+function removeStorageSync(key: string): boolean {
+    try {
+        uni.removeStorageSync(key)
+        return true
+    } catch (error) {
+        console.warn(`移除存储失败: ${key}`, error)
+        return false
+    }
+}
+
+// ================== 主函数 ==================
 
 export function useAppUpdate() {
     const { t } = useI18n()
@@ -16,54 +215,81 @@ export function useAppUpdate() {
     const checking = ref(false)
     const updating = ref(false)
 
-    let downloadTask: any = null
-    let networkListener: any = null
+    let downloadTask: PlusDownloaderDownload | null = null
+    let networkListener: ((res: any) => void) | null = null
     let downloadUrl = ''
-    let savedFilePath = '_downloads/app_update.wgt'
-    let currentNetworkType = ''
+    let retryCount = 0
+    let stateChangeHandler: ((d: any) => void) | null = null
 
-    /* ================== 对外入口 ================== */
+    // ================== 对外入口 ==================
 
-    async function checkUpdate() {
+    /**
+     * 检查应用更新
+     */
+    async function checkUpdate(): Promise<void> {
         // #ifdef APP-PLUS
-        if (checking.value) return
+        if (checking.value) {
+            console.warn('更新检查已在进行中')
+            return
+        }
+
         checking.value = true
 
         try {
-            const platform = uni.getSystemInfoSync().platform
-            const equipmentType = platform === 'ios' ? 'ios' : 'Android'
-            const res = await ucardApi.getAppVersionDetail({ equipmentType })
-            if (res.code !== 200 || !res.data) return
+            const equipmentType = getEquipmentType()
+            const res: ApiResponse<UpdateInfo> = await ucardApi.getAppVersionDetail({ equipmentType })
+
+            if (res.code !== 200 || !res.data) {
+                const errorMsg = res.message || '获取版本信息失败'
+                console.warn(errorMsg)
+                return
+            }
 
             const update = res.data
             const currentVersion = await getCurrentVersion()
             const needUpdate = compareVersion(update.version, currentVersion) > 0
 
+            // 保存版本信息
             userStore.saveAppVersion({
                 currentVersion,
                 version: update.version,
                 isUpdate: !needUpdate
             })
 
-            if (!needUpdate) return
+            if (!needUpdate) {
+                console.log('当前已是最新版本')
+                return
+            }
 
-            const skip = uni.getStorageSync(SKIP_VERSION_KEY)
-            if (!update.forceUpdate && skip === update.version) return
+            // 检查是否已跳过此版本
+            // const skipVersion = getStorageSync<string>(STORAGE_KEYS.SKIP_VERSION)
+            // if (!update.forceUpdate && skipVersion === update.version) {
+            //     console.log('用户已跳过此版本更新')
+            //     return
+            // }
 
+            // 显示更新提示
             if (update.forceUpdate) {
                 showForceUpdate(update)
             } else {
                 showOptionalUpdate(update)
             }
+        } catch (error) {
+            console.error('检查更新失败:', error)
+            const errorMsg = error instanceof Error ? error.message : String(error)
+            showToast(t('mine.p28') || `检查更新失败: ${errorMsg}`)
         } finally {
             checking.value = false
         }
         // #endif
     }
 
-    /* ================== 更新流程 ================== */
+    // ================== 更新流程 ==================
 
-    function showForceUpdate(update: any) {
+    /**
+     * 显示强制更新弹窗
+     */
+    function showForceUpdate(update: UpdateInfo): void {
         uni.showModal({
             title: t('mine.p22'),
             content: t('mine.p23'),
@@ -73,185 +299,401 @@ export function useAppUpdate() {
         })
     }
 
-    function showOptionalUpdate(update: any) {
+    /**
+     * 显示可选更新弹窗
+     */
+    function showOptionalUpdate(update: UpdateInfo): void {
         uni.showModal({
             title: t('mine.p24'),
-            content: t('mine.p25', { version: 'v' + update.version }),
+            content: t('mine.p25', { version: `v${update.version}` }),
             confirmText: t('mine.p35'),
             cancelText: t('mine.p36'),
-            success: res => {
-                if (res.confirm) doUpdate(update)
-                else uni.setStorageSync(SKIP_VERSION_KEY, update.version)
+            success: (res) => {
+                if (res.confirm) {
+                    doUpdate(update)
+                } else {
+                    // 记录跳过的版本
+                    setStorageSync(STORAGE_KEYS.SKIP_VERSION, update.version)
+                }
             }
         })
     }
 
-    function doUpdate(update: any) {
-        const platform = uni.getSystemInfoSync().platform
+    /**
+     * 执行更新
+     */
+    function doUpdate(update: UpdateInfo): void {
+        const platform = getPlatform()
+        
         if (platform === 'ios') {
+            handleIosUpdate(update)
+        } else {
+            handleAndroidUpdate(update)
+        }
+    }
+
+    /**
+     * 处理 iOS 更新
+     */
+    function handleIosUpdate(update: UpdateInfo): void {
+        if (!update.iosStoreUrl) {
+            showToast(t('mine.p28') || 'iOS 更新链接不存在')
+            return
+        }
+
+        try {
             plus.runtime.openURL(update.iosStoreUrl)
+        } catch (error) {
+            console.error('打开 App Store 失败:', error)
+            showToast(t('mine.p28') || '打开 App Store 失败')
+        }
+    }
+
+    /**
+     * 处理 Android 更新
+     */
+    function handleAndroidUpdate(update: UpdateInfo): void {
+        if (!update.wgtUrl) {
+            showToast(t('mine.p28') || '更新包链接不存在')
             return
         }
+
         downloadWgt(update.wgtUrl)
     }
 
+    // ================== 下载核心 ==================
 
-    /* ================== 下载核心 ================== */
-
-    function downloadWgt(url: string) {
+    /**
+     * 下载 wgt 更新包
+     */
+    function downloadWgt(url: string): void {
         // #ifdef APP-PLUS
-        if (downloadTask) return
+        if (downloadTask) {
+            console.warn('下载任务已存在')
+            return
+        }
+
+        if (!url || !isValidUrl(url)) {
+            handleDownloadFail(t('mine.p28') || '下载链接无效')
+            return
+        }
+
         downloadUrl = url
+        retryCount = 0
         updating.value = true
         progress.show(t('mine.p29'))
-        // 获取当前网络类型
-        uni.getNetworkType({
-            success: (res) => {
-                currentNetworkType = res.networkType
-            }
-        })
+
+        // 初始化网络监听
+        initNetworkType()
         startNetworkMonitor()
-        downloadTask = plus.downloader.createDownload(
-            url,
-            { filename: savedFilePath },
-            (download, status) => {
-                if (status === 200 || status === 206) {
-                    clearCache()
-                    stopNetworkMonitor()
-                    installWgt(download.filename)
-                } else {
-                    fail(t('mine.p28'))
-                }
+
+        createDownloadTask(url)
+        // #endif
+    }
+
+    /**
+     * 创建下载任务
+     */
+    function createDownloadTask(url: string): void {
+        try {
+            downloadTask = plus.downloader.createDownload(
+                url,
+                { filename: DOWNLOAD_CONFIG.FILE_PATH },
+                handleDownloadComplete
+            )
+
+            if (!downloadTask) {
+                throw new Error('创建下载任务失败')
             }
-        )
-        downloadTask.addEventListener('statechanged', (d: any) => {
-            const state = d.state
-            if (state === 3 && d.totalSize) { // 下载中
-                const p = Math.floor((d.downloadedSize / d.totalSize) * 100)
-                progress.update(p, `${t('mine.p29')} ${p}%`)
-                saveCache(p)
+
+            // 创建状态变化处理器
+            stateChangeHandler = handleDownloadStateChanged
+            downloadTask.addEventListener('statechanged', stateChangeHandler)
+            downloadTask.start()
+        } catch (error) {
+            console.error('创建下载任务失败:', error)
+            downloadTask = null
+            handleDownloadFail(t('mine.p28') || '创建下载任务失败')
+        }
+    }
+
+    /**
+     * 验证 URL 格式
+     */
+    function isValidUrl(url: string): boolean {
+        try {
+            return /^https?:\/\/.+/.test(url)
+        } catch {
+            return false
+        }
+    }
+
+    /**
+     * 处理下载完成回调
+     */
+    function handleDownloadComplete(download: PlusDownloaderDownload, status: number): void {
+        const isSuccess = status === HttpStatus.OK || status === HttpStatus.PARTIAL_CONTENT
+
+        if (isSuccess) {
+            console.log('下载成功,准备安装')
+            clearCache()
+            stopNetworkMonitor()
+            installWgt(download.filename)
+        } else {
+            console.error('下载失败,状态码:', status)
+            retryDownload()
+        }
+    }
+
+    /**
+     * 处理下载状态变化
+     */
+    function handleDownloadStateChanged(event: DownloadStateEvent): void {
+        if (event.state === DownloadState.DOWNLOADING && event.totalSize > 0) {
+            const percent = Math.min(100, Math.floor((event.downloadedSize / event.totalSize) * 100))
+            const progressText = `${t('mine.p29')} ${percent}%`
+            progress.update(percent, progressText)
+            saveCache(percent)
+        }
+    }
+
+    /**
+     * 重试下载
+     */
+    function retryDownload(): void {
+        if (retryCount >= DOWNLOAD_CONFIG.MAX_RETRY) {
+            console.error(`下载失败,已重试 ${retryCount} 次`)
+            handleDownloadFail(t('mine.p28') || '下载失败')
+            return
+        }
+
+        retryCount++
+        console.log(`下载失败,${DOWNLOAD_CONFIG.RETRY_DELAY / 1000}秒后重试 (${retryCount}/${DOWNLOAD_CONFIG.MAX_RETRY})`)
+        
+        cleanupDownloadTask()
+        
+        setTimeout(() => {
+            if (downloadUrl) {
+                createDownloadTask(downloadUrl)
+            }
+        }, DOWNLOAD_CONFIG.RETRY_DELAY)
+    }
+
+    /**
+     * 初始化网络类型
+     */
+    function initNetworkType(): void {
+        uni.getNetworkType({
+            success: (res) => {
+                console.log('当前网络类型:', res.networkType)
+            },
+            fail: (error) => {
+                console.warn('获取网络类型失败:', error)
             }
         })
-        downloadTask.start()
-        // #endif
     }
-    /* ================== 网络处理 ================== */
 
-    function startNetworkMonitor() {
+    // ================== 网络处理 ==================
+
+    /**
+     * 启动网络监听
+     */
+    function startNetworkMonitor(): void {
         // #ifdef APP-PLUS
-        networkListener = uni.onNetworkStatusChange(res => {
-            if (!downloadTask || !updating.value) return
-            // 只要有网络变化,直接尝试恢复下载
+        if (networkListener) {
+            console.warn('网络监听已存在')
+            return
+        }
+
+        networkListener = (res: any) => {
+            if (!downloadTask || !updating.value) {
+                return
+            }
+
             if (!res.isConnected) {
-                currentNetworkType = 'none'
-                try {
-                    downloadTask.pause && downloadTask.pause()
-                } catch (e) {
-                    console.error('暂停下载失败:', e)
-                }
+                // 网络断开,暂停下载
+                pauseDownload()
                 return
             }
 
-            // 网络恢复或切换,直接 resume,不依赖 state
-            currentNetworkType = res.networkType
+            // 网络恢复,继续下载
+            resumeDownload()
+        }
+
+        uni.onNetworkStatusChange(networkListener)
+        // #endif
+    }
+
+    /**
+     * 暂停下载
+     */
+    function pauseDownload(): void {
+        if (!downloadTask) return
+
+        try {
+            if (typeof downloadTask.pause === 'function') {
+                downloadTask.pause()
+            }
+        } catch (error) {
+            console.error('暂停下载失败:', error)
+        }
+    }
+
+    /**
+     * 恢复下载
+     */
+    function resumeDownload(): void {
+        if (!downloadTask) return
+
+        try {
+            if (typeof downloadTask.resume === 'function') {
+                downloadTask.resume()
+            } else if (typeof downloadTask.start === 'function') {
+                downloadTask.start()
+            }
+        } catch (error) {
+            console.error('恢复下载失败,尝试重新开始:', error)
+            // 失败时尝试重新开始
             try {
-                if (downloadTask.resume) {
-                    downloadTask.resume()
-                } else {
-                    downloadTask.start()
-                }
-            } catch (e) {
-                // 失败时尝试用 start 重试
-                try {
+                if (downloadTask.start) {
                     downloadTask.start()
-                } catch (e2) {
-                    console.error('start() 也失败:', e2)
                 }
+            } catch (e2) {
+                console.error('重新开始下载也失败:', e2)
             }
-        })
-        // #endif
+        }
     }
-    function stopNetworkMonitor() {
+
+    /**
+     * 停止网络监听
+     */
+    function stopNetworkMonitor(): void {
         if (networkListener) {
             uni.offNetworkStatusChange(networkListener)
             networkListener = null
         }
-        currentNetworkType = ''
     }
 
-    /* ================== 安装 ================== */
-
-    function installWgt(path: string) {
-        plus.runtime.install(
-            path,
-            { force: false },
-            () => {
-                progress.hide()
-                updating.value = false
-                uni.showModal({
-                    title: t('mine.p37'),
-                    content: t('mine.p38'),
-                    showCancel: false,
-                    success: () => plus.runtime.restart()
-                })
-            },
-            () => fail(t('mine.p33'))
-        )
+    // ================== 安装 ==================
+
+    /**
+     * 安装 wgt 更新包
+     */
+    function installWgt(path: string): void {
+        // #ifdef APP-PLUS
+        try {
+            plus.runtime.install(
+                path,
+                { force: false },
+                () => {
+                    // 安装成功
+                    progress.hide()
+                    updating.value = false
+                    uni.showModal({
+                        title: t('mine.p37'),
+                        content: t('mine.p38'),
+                        showCancel: false,
+                        success: () => {
+                            plus.runtime.restart()
+                        }
+                    })
+                },
+                (error) => {
+                    // 安装失败
+                    console.error('安装失败:', error)
+                    handleDownloadFail(t('mine.p33'))
+                }
+            )
+        } catch (error) {
+            console.error('安装异常:', error)
+            handleDownloadFail(t('mine.p33'))
+        }
+        // #endif
     }
 
-    /* ================== 缓存(仅 UI) ================== */
+    // ================== 缓存管理 ==================
 
-    function saveCache(progress: number) {
-        uni.setStorageSync(DOWNLOAD_CACHE_KEY, {
+    /**
+     * 保存下载缓存
+     */
+    function saveCache(progressPercent: number): void {
+        const cache: DownloadCache = {
             url: downloadUrl,
-            progress,
+            progress: progressPercent,
             time: Date.now()
-        })
+        }
+        setStorageSync(STORAGE_KEYS.DOWNLOAD_CACHE, cache)
     }
 
-    function clearCache() {
-        uni.removeStorageSync(DOWNLOAD_CACHE_KEY)
+    /**
+     * 清除下载缓存
+     */
+    function clearCache(): void {
+        removeStorageSync(STORAGE_KEYS.DOWNLOAD_CACHE)
     }
 
-    /* ================== 兜底失败 ================== */
+    // ================== 错误处理 ==================
 
-    function fail(msg: string) {
+    /**
+     * 处理下载失败
+     */
+    function handleDownloadFail(msg: string): void {
         progress.hide()
         updating.value = false
+        retryCount = 0
         stopNetworkMonitor()
-        if (downloadTask) {
-            try { downloadTask.abort() } catch { }
+        cleanupDownloadTask()
+        showToast(msg)
+    }
+
+    /**
+     * 清理下载任务
+     */
+    function cleanupDownloadTask(): void {
+        if (!downloadTask) return
+
+        try {
+            // 移除事件监听
+            if (stateChangeHandler && downloadTask.removeEventListener) {
+                downloadTask.removeEventListener('statechanged', stateChangeHandler)
+            }
+            
+            // 中止下载
+            downloadTask.abort()
+        } catch (error) {
+            console.warn('清理下载任务失败:', error)
+        } finally {
             downloadTask = null
+            stateChangeHandler = null
         }
-        uni.showToast({ title: msg, icon: 'none' })
     }
 
-    onUnmounted(() => {
-        stopNetworkMonitor()
-    })
+    /**
+     * 取消更新
+     */
+    function cancelUpdate(): void {
+        if (!updating.value) {
+            console.warn('当前没有正在进行的更新')
+            return
+        }
 
-    return { checkUpdate }
-}
+        console.log('用户取消更新')
+        handleDownloadFail(t('mine.p39') || '已取消更新')
+    }
 
-/* ================== 工具 ================== */
+    // ================== 生命周期 ==================
 
-function getCurrentVersion(): Promise<string> {
-    return new Promise(resolve => {
-        plus.runtime.getProperty(plus.runtime.appid, info => {
-            resolve(info.version)
-        })
+    onUnmounted(() => {
+        console.log('useAppUpdate 组件卸载,清理资源')
+        stopNetworkMonitor()
+        cleanupDownloadTask()
+        retryCount = 0
     })
-}
 
-function compareVersion(v1: string, v2: string) {
-    const a1 = v1.split('.')
-    const a2 = v2.split('.')
-    const len = Math.max(a1.length, a2.length)
-    for (let i = 0; i < len; i++) {
-        const n1 = parseInt(a1[i] || '0')
-        const n2 = parseInt(a2[i] || '0')
-        if (n1 > n2) return 1
-        if (n1 < n2) return -1
+    return {
+        checkUpdate,
+        cancelUpdate,
+        checking,
+        updating
     }
-    return 0
 }

+ 6 - 1
locale/cn.json

@@ -74,7 +74,12 @@
       "index": "",
       "balance": "钱包余额记录",
       "global-list": "速汇记录",
-      "global-order": "CWG速汇"
+      "global-order": "CWG速汇",
+      "withdraw": "提现",
+      "vaultody": "充值",
+      "withdraw-list": "钱包提现记录",
+      "vaultody-list": "钱包充值记录",
+      "withdraw-detail": "提现详情"
     }
   },
   "tabBar": {

+ 3 - 2
manifest.json

@@ -2,7 +2,7 @@
     "name" : "C Pays",
     "appid" : "__UNI__60A9455",
     "description" : "应用描述",
-    "versionName" : "1.0.1",
+    "versionName" : "1.0.3",
     "versionCode" : 2,
     "transformPx" : false,
     "app-plus" : {
@@ -24,7 +24,8 @@
             "Share" : {},
             "Speech" : {},
             "VideoPlayer" : {},
-            "Camera" : {}
+            "Camera" : {},
+            "Barcode" : {}
         },
         "distribute" : {
             "android" : {

+ 2 - 1
package.json

@@ -5,7 +5,8 @@
   "main": "main.js",
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1",
-    "clean": "bash clean.sh"
+    "clean": "bash clean.sh",
+    "svgicon": "node ./uni_modules/cwg-svg-icon/tools/generate-svg-icon.js"
   },
   "keywords": [],
   "author": "",

+ 35 - 0
pages.json

@@ -178,6 +178,41 @@
 				"navigationStyle": "custom"
 			}
 		},
+		{
+			"path": "pages/wallet/vaultody",
+			"style": {
+				"navigationBarTitleText": "",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/wallet/vaultody-list",
+			"style": {
+				"navigationBarTitleText": "",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/wallet/withdraw",
+			"style": {
+				"navigationBarTitleText": "",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/wallet/withdraw-list",
+			"style": {
+				"navigationBarTitleText": "",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/wallet/withdraw-detail",
+			"style": {
+				"navigationBarTitleText": "",
+				"navigationStyle": "custom"
+			}
+		},
 		{
 			"path": "pages/login/index",
 			"style": {

+ 0 - 3
pages/card/components/FirstApply.vue

@@ -35,9 +35,6 @@
       </view>
       <view v-t="'card.Info.s21'" class="f"></view>
     </view>
-    <!-- <view class="cwg-button">
-            <van-button type="primary" block @click="handleApply">{{ t('cards.p4') }}</van-button>
-        </view> -->
   </view>
 </template>
 

+ 12 - 0
pages/mine/index.vue

@@ -32,6 +32,18 @@
 					<text class="version item-text"></text>
 					<cwg-icon color="#000" icon="chevron-right" />
 				</view>
+				<view class="group-item" @click="router.push('/pages/wallet/vaultody-list')">
+					<cwg-icon color="#000" icon="qbye" />
+					<text class="item-text">钱包充值记录</text>
+					<text class="version item-text"></text>
+					<cwg-icon color="#000" icon="chevron-right" />
+				</view>
+				<view class="group-item" @click="router.push('/pages/wallet/withdraw-list')">
+					<cwg-icon color="#000" icon="qbye" />
+					<text class="item-text">钱包提现记录</text>
+					<text class="version item-text"></text>
+					<cwg-icon color="#000" icon="chevron-right" />
+				</view>
 				<view class="group-item" @click="router.push('/pages/mine/help')">
 					<cwg-icon color="#000" icon="cjwt" />
 					<text class="item-text">{{ t('pages.mine.help') }}</text>

+ 14 - 68
pages/recharge-record/components/DeductionList.vue

@@ -1,8 +1,8 @@
 <template>
   <cwg-load-more-wrapper ref="loadMoreWrapperRef" :loading="loading" :finished="finished" :height='108'
     :refresher-enabled="true" @reach-bottom="loadMore" @refresh="handleRefresh">
-    <view v-if="filteredRecords.length > 0" class="records-list">
-      <view v-for="record in filteredRecords" :key="record.id" class="record-card" @click="goToDeductionDetail(record)">
+    <view v-if="records.length > 0" class="records-list">
+      <view v-for="record in records" :key="record.id" class="record-card" @click="goToDeductionDetail(record)">
         <view class="record-main">
           <view class="record-left">
             <view class="type-icon deduction-icon">
@@ -121,12 +121,6 @@ const getStatusColor = (status?: string | number): string => {
 const getStatusBadgeClass = (status?: string | number) => `status-${normalizeStatus(status)}`;
 const getStatusTextClass = (status?: string | number) => `status-text-${normalizeStatus(status)}`;
 
-const getStatusValue = (index: number): NormalizedStatus | null => {
-  if (index === 0) return null;
-  const statusMap: NormalizedStatus[] = ['success', 'processing', 'failed'];
-  return statusMap[index - 1];
-};
-
 const getDeductionTypeText = (type?: string | number): string => {
   if (!type) return '--';
   const typeNum = typeof type === 'string' ? parseInt(type) : type;
@@ -163,45 +157,6 @@ const formatDateTime = (time?: string | number): string => {
   }
 };
 
-const getDatePart = (time?: string | number): string => {
-  if (!time) return '';
-  try {
-    let date: dayjs.Dayjs;
-    if (typeof time === 'number') {
-      if (time.toString().length === 10) {
-        date = dayjs.unix(time);
-      } else {
-        date = dayjs(time);
-      }
-    } else {
-      date = dayjs(time);
-    }
-    if (!date.isValid()) return '';
-    return date.format('YYYY-MM-DD');
-  } catch (error) {
-    return '';
-  }
-};
-
-const formatOrderNo = (orderNo?: string) => {
-  if (!orderNo) return '--';
-  if (orderNo.length <= 20) return orderNo;
-  return orderNo.slice(0, 6) + '...' + orderNo.slice(-4);
-};
-
-const copyOrderNo = (orderNo?: string) => {
-  if (!orderNo) return;
-  uni.setClipboardData({
-    data: orderNo,
-    success: () => {
-      uni.showToast({
-        title: t('card.Msg.m8') || '复制成功',
-        icon: 'success'
-      });
-    }
-  });
-};
-
 const fetchRecords = async (isLoadMore = false) => {
   if (!props.cardNo || loading.value) return;
   if (isLoadMore && finished.value) return;
@@ -210,6 +165,8 @@ const fetchRecords = async (isLoadMore = false) => {
   try {
     const res = await ucardApi.getCardWithdrawPage({
       cardNo: props.cardNo,
+      type: props.typeIndex || undefined,
+      status: props.statusIndex || undefined,
       beginDate: props.dateFilter ? dayjs(props.dateFilter).format('YYYY-MM-DD') : undefined,
       endDate: props.dateFilter ? dayjs(props.dateFilter).format('YYYY-MM-DD') : undefined,
       page: { current: page.value, row: pageSize },
@@ -273,32 +230,21 @@ const loadMore = () => {
   fetchRecords(true);
 };
 
-const filteredRecords = computed(() => {
-  return records.value.filter(record => {
-    const type = record.typeStr || record.type;
-    if (props.typeIndex > 0 && type !== props.typeOptions[props.typeIndex]) {
-      return false;
-    }
-    const statusValue = getStatusValue(props.statusIndex);
-    if (statusValue && normalizeStatus(record.status) !== statusValue) {
-      return false;
-    }
-    if (props.dateFilter) {
-      const time = record.addTime || record.time;
-      const datePart = getDatePart(time);
-      if (!datePart || datePart !== props.dateFilter) {
-        return false;
-      }
-    }
-    return true;
-  });
-});
-
 watch([() => props.dateFilter], () => {
   page.value = 1;
   finished.value = false;
   fetchRecords();
 }, { immediate: false });
+watch([() => props.typeIndex], () => {
+  page.value = 1;
+  finished.value = false;
+  fetchRecords();
+}, { immediate: false });
+watch([() => props.statusIndex], () => {
+  page.value = 1;
+  finished.value = false;
+  fetchRecords();
+}, { immediate: false });
 
 const loadMoreWrapperRef = ref<any>(null);
 

+ 16 - 32
pages/recharge-record/components/RechargeList.vue

@@ -1,11 +1,11 @@
 <template>
   <cwg-load-more-wrapper ref="loadMoreWrapperRef" :loading="loading" :finished="finished" :height='108'
     :refresher-enabled="pageSize !== 4" @reach-bottom="loadMore" @refresh="handleRefresh">
-    <view v-if="filteredRecords.length > 0" :class="{
+    <view v-if="records.length > 0" :class="{
       'records-list': true,
       'records-list1': pageSize === 4
     }">
-      <view v-for="record in filteredRecords" :key="record.id" class="record-card" @click="goToRechargeDetail(record)">
+      <view v-for="record in records" :key="record.id" class="record-card" @click="goToRechargeDetail(record)">
         <view class="record-main">
           <view class="record-left">
             <view class="type-icon recharge-icon">
@@ -29,7 +29,7 @@
 
           <view class="record-right">
             <text class="amount-recharge">+{{ Number(record.amount || 0).toFixed(2) }} {{ record.currency || 'USD'
-              }}</text>
+            }}</text>
             <text class="fee-text">{{ t('global.GlobalOrder.fee') }} {{ Number(record.fee || 0).toFixed(2) }}</text>
           </view>
         </view>
@@ -57,7 +57,7 @@ import { showToast } from '@/utils/toast';
 import { ucardApi, TransactionInfo } from '@/api/ucard';
 import { rechargeStatusMap } from '@/utils/dataMap';
 import useCardStore from '@/stores/use-card-store';
-import {rechargeType} from '@/utils/dataMap';
+import { rechargeType } from '@/utils/dataMap';
 const rechargeTypes = computed(() => rechargeType);
 interface RecordItem extends TransactionInfo {
   type?: string;
@@ -133,11 +133,6 @@ const getStatusColor = (status?: string | number): string => {
 const getStatusBadgeClass = (status?: string | number) => `status-${normalizeStatus(status)}`;
 const getStatusTextClass = (status?: string | number) => `status-text-${normalizeStatus(status)}`;
 
-const getStatusValue = (index: number): NormalizedStatus | null => {
-  if (index === 0) return null;
-  const statusMap: NormalizedStatus[] = ['success', 'processing', 'failed'];
-  return statusMap[index - 1];
-};
 
 const formatDateTime = (time?: string | number): string => {
   if (!time) return '--';
@@ -206,6 +201,8 @@ const fetchRecords = async (isLoadMore = false) => {
   try {
     const res = await ucardApi.rechargeList({
       cardNo: props.cardNo,
+      rechargeType: props.typeIndex || undefined,
+      status: props.statusIndex || undefined,
       startDate: props.dateFilter ? dayjs(props.dateFilter).format('YYYY-MM-DD') : undefined,
       endDate: props.dateFilter ? dayjs(props.dateFilter).format('YYYY-MM-DD') : undefined,
       page: { current: page.value, row: pageSize.value },
@@ -217,7 +214,6 @@ const fetchRecords = async (isLoadMore = false) => {
     } else {
       records.value = data;
     }
-
     if (data.length < pageSize.value) {
       finished.value = true;
     } else {
@@ -268,26 +264,6 @@ const loadMore = () => {
   fetchRecords(true);
 };
 
-const filteredRecords = computed(() => {
-  return records.value.filter(record => {
-    const type = record.typeStr || record.type;
-    if (props.typeIndex > 0 && type !== props.typeOptions[props.typeIndex]) {
-      return false;
-    }
-    const statusValue = getStatusValue(props.statusIndex);
-    if (statusValue && normalizeStatus(record.status) !== statusValue) {
-      return false;
-    }
-    if (props.dateFilter) {
-      const time = record.addTime || record.time;
-      const datePart = getDatePart(time);
-      if (!datePart || datePart !== props.dateFilter) {
-        return false;
-      }
-    }
-    return true;
-  });
-});
 
 // 监听筛选条件变化,重新加载数据
 watch([() => props.dateFilter], () => {
@@ -295,6 +271,16 @@ watch([() => props.dateFilter], () => {
   finished.value = false;
   fetchRecords();
 }, { immediate: false });
+watch([() => props.typeIndex], () => {
+  page.value = 1;
+  finished.value = false;
+  fetchRecords();
+}, { immediate: false });
+watch([() => props.statusIndex], () => {
+  page.value = 1;
+  finished.value = false;
+  fetchRecords();
+}, { immediate: false });
 
 const loadMoreWrapperRef = ref<any>(null);
 
@@ -332,8 +318,6 @@ defineExpose({
 }
 
 .records-list1 {
-  flex-direction: row;
-  flex-wrap: wrap;
   gap: px2rpx(12);
   padding: px2rpx(16) 0;
 }

+ 14 - 24
pages/recharge-record/components/TransactionList.vue

@@ -1,12 +1,11 @@
 <template>
   <cwg-load-more-wrapper ref="loadMoreWrapperRef" :loading="loading" :finished="finished" :height='108'
     :refresher-enabled="pageSize !== 4" @reach-bottom="loadMore" @refresh="handleRefresh">
-    <view v-if="filteredRecords.length > 0" :class="{
+    <view v-if="records.length > 0" :class="{
       'records-list': true,
       'records-list1': pageSize === 4
     }">
-      <view v-for="record in filteredRecords" :key="record.id" class="record-card"
-        @click="goToTransactionDetail(record)">
+      <view v-for="record in records" :key="record.id" class="record-card" @click="goToTransactionDetail(record)">
         <view class="record-main">
           <view class="record-left">
             <view class="type-icon transaction-icon">
@@ -222,6 +221,8 @@ const fetchRecords = async (isLoadMore = false) => {
   try {
     const res = await ucardApi.transactionsList({
       cardNo: props.cardNo,
+      type: props.typeIndex || undefined,
+      status: props.statusIndex || undefined,
       beginDate: props.dateFilter ? dayjs(props.dateFilter).format('YYYY-MM-DD') : undefined,
       endDate: props.dateFilter ? dayjs(props.dateFilter).format('YYYY-MM-DD') : undefined,
       page: { current: page.value, row: pageSize.value },
@@ -284,33 +285,22 @@ const loadMore = () => {
   fetchRecords(true);
 };
 
-const filteredRecords = computed(() => {
-  return records.value.filter(record => {
-    const type = record.typeStr || record.type;
-    if (props.typeIndex > 0 && type !== props.typeOptions[props.typeIndex]) {
-      return false;
-    }
-    const statusValue = getStatusValue(props.statusIndex);
-    if (statusValue && normalizeStatus(record.status) !== statusValue) {
-      return false;
-    }
-    if (props.dateFilter) {
-      const time = record.transactionTime;
-      const datePart = getDatePart(time);
-      if (!datePart || datePart !== props.dateFilter) {
-        return false;
-      }
-    }
-    return true;
-  });
-});
 
 watch([() => props.dateFilter], () => {
   page.value = 1;
   finished.value = false;
   fetchRecords();
 }, { immediate: false });
-
+watch([() => props.typeIndex], () => {
+  page.value = 1;
+  finished.value = false;
+  fetchRecords();
+}, { immediate: false });
+watch([() => props.statusIndex], () => {
+  page.value = 1;
+  finished.value = false;
+  fetchRecords();
+}, { immediate: false });
 const loadMoreWrapperRef = ref<any>(null);
 
 const refresh = async () => {

+ 0 - 214
pages/recharge-record/detail copy.vue

@@ -1,214 +0,0 @@
-<template>
-  <cwg-page-wrapper>
-    <view class="page">
-      <view class="success-icon-wrap">
-        <img src="/static/images/vector.png" alt="" />
-      </view>
-      <view class="amount">{{ transactionInfo?.amount || 0 }}
-        {{ transactionInfo?.receivedCurrency || "USD" }}</view>
-      <view class="detail-card">
-        <view class="detail-row one">
-          <text class="label">{{ t("card.Form.f53") }}</text>
-          <text v-if="transactionInfo?.approveStatus" class=""
-            :class="[statusClass1(transactionInfo?.approveStatus)]">{{ statusMap1[transactionInfo?.approveStatus]
-            }}</text>
-        </view>
-        <view class="detail-row one">
-          <text class="label">{{ t("card.Form.f54") }}</text>
-          <text v-if="transactionInfo?.status" class="" :class="[statusClass(transactionInfo?.status)]">{{
-            statusMap[transactionInfo?.status] }}</text>
-        </view>
-        <view class="detail-row">
-          <text class="label">{{ t("recharge-record-detail.p7") }}</text>
-          <text class="value">{{
-            transactionInfo?.merchantOrderNo || "--"
-          }}</text>
-        </view>
-        <view class="detail-row">
-          <text class="label">{{ t("recharge-record-detail.p8") }}</text>
-          <text class="value">{{ transactionInfo?.cardNumber || "--" }}</text>
-        </view>
-        <view class="detail-row">
-          <text class="label">{{ t("card.Form.f30") }}</text>
-          <text class="value">{{ transactionInfo?.fee || 0 }} USD</text>
-        </view>
-        <view class="detail-row">
-          <text class="label">{{ t("recharge-record-detail.p12") }}</text>
-          <text class="value">
-            {{ transactionInfo?.addTime }}
-          </text>
-        </view>
-      </view>
-      <view class="fixed-btn">
-        <view class="cwg-button">
-          <u-button type="primary" block @click="
-            () => {
-              router.back();
-            }
-          ">
-            {{ t("recharge-record-detail.b1") }}
-          </u-button>
-        </view>
-      </view>
-    </view>
-  </cwg-page-wrapper>
-</template>
-
-<script setup lang="ts">
-import { ref, onMounted, watch, computed } from "vue";
-import { showToast } from "@/utils/toast";
-import { onLoad } from '@dcloudio/uni-app'
-import useRouter from "@/hooks/useRouter";
-import { useI18n } from "vue-i18n";
-import { ucardApi } from "@/api/ucard";
-
-const router = useRouter();
-const merchantOrderNo = ref("");
-const { t } = useI18n();
-onLoad((options) => {
-  merchantOrderNo.value = options.merchantOrderNo
-
-})
-
-const transactionInfo = ref<TransactionInfo | null>(null);
-async function getTransactionInfo() {
-  const res = await ucardApi.ucardRechargeOrder({
-    merchantOrderNo: merchantOrderNo.value,
-  });
-  transactionInfo.value = res.data;
-}
-
-function statusClass(status: string) {
-  switch (status) {
-    case "success":
-      return "status-default status-success";
-    case "fail":
-      return "status-default status-error";
-    default:
-      return "status-default";
-  }
-}
-const statusMap = {
-  success: t("card.Status.t1"),
-  fail: t("card.Status.t2"),
-  processing: t("card.Status.t3"),
-  wait_process: t("card.Status.t3"),
-};
-function statusClass1(status: string) {
-  switch (status) {
-    case 2:
-      return "status-default status-success";
-    case 3:
-      return "status-default status-error";
-    default:
-      return "status-default";
-  }
-}
-const statusMap1 = {
-  2: t("card.Status.t14"),
-  3: t("card.Status.t15"),
-  1: t("card.Status.t13"),
-};
-onMounted(() => {
-  getTransactionInfo();
-});
-</script>
-
-<style scoped lang="scss">
-@import "@/uni.scss";
-
-.success-icon-wrap {
-  display: flex;
-  justify-content: center;
-  align-items: center;
-}
-
-.success-icon {
-  display: block;
-}
-
-.detail-card {
-  border-radius: px2rpx(12);
-  background: #fff;
-  box-shadow: 0px 4px 30px 0px rgba(0, 0, 0, 0.07);
-  display: flex;
-  padding: px2rpx(44) px2rpx(31) px2rpx(28) px2rpx(31);
-  flex-direction: column;
-}
-
-.detail-row {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: px2rpx(31) 0;
-  font-size: px2rpx(31);
-}
-
-.detail-row:last-child {
-  border-bottom: none;
-}
-
-.label {
-  color: #8e8a8a;
-  font-family: Roboto;
-  font-size: px2rpx(14);
-  font-style: normal;
-  font-weight: 600;
-  line-height: px2rpx(20);
-  letter-spacing: px2rpx(0.14);
-}
-
-.value {
-  color: #000;
-
-  text-align: right;
-  /* Title2 */
-  font-family: Roboto;
-  font-size: px2rpx(14);
-  font-style: normal;
-  font-weight: 600;
-  line-height: px2rpx(20);
-  letter-spacing: px2rpx(0.14);
-}
-
-.value.success {
-  padding: px2rpx(4) px2rpx(8);
-  border-radius: px2rpx(30);
-  background: rgba(76, 175, 80, 0.2);
-  color: #4caf50;
-  font-family: Roboto;
-  font-size: px2rpx(12);
-  font-style: normal;
-  font-weight: 600;
-  line-height: px2rpx(31);
-  letter-spacing: px2rpx(0.06);
-}
-
-.value.processing {
-  padding: px2rpx(4) px2rpx(8);
-  border-radius: px2rpx(30);
-  background: rgba(0, 157, 235, 0.2);
-  color: #009deb;
-  font-family: Roboto;
-  font-size: px2rpx(12);
-  font-style: normal;
-  font-weight: 600;
-  line-height: px2rpx(31);
-  letter-spacing: px2rpx(0.06);
-}
-
-.amount {
-  color: #1a1a1a;
-  text-align: center;
-  font-family: Roboto;
-  font-size: px2rpx(22);
-  font-style: normal;
-  font-weight: 600;
-  line-height: px2rpx(28);
-  margin: px2rpx(12) 0 px2rpx(62) 0;
-}
-
-.one {
-  border-bottom: 1px dashed #beb6b6;
-}
-</style>

+ 57 - 84
pages/recharge-record/detail.vue

@@ -1,23 +1,21 @@
 <template>
   <cwg-page-wrapper>
     <view class="order-detail-page">
-
-      <!-- Content -->
       <view class="content">
-        <!-- Status Card -->
-        <view class="status-card">
-          <view class="status-icon-wrapper">
-            <view :class="['status-icon', `status-icon-${orderDetail.orderStatus}`]">
-              <uni-icons :type="getOrderStatusIcon(orderDetail.orderStatus)" size="40"
-                :color="getOrderStatusColor(orderDetail.orderStatus)" />
-            </view>
+        <view class="status-card section-card">
+          <view class="success-icon-wrapper">
+            <image v-if="orderDetail.orderStatus === 'success'" src="/static/images/vector.png" alt=""
+              mode="widthFix" />
+            <image v-else-if="orderDetail.orderStatus === 'fail'" src="/static/images/vector2.png" alt=""
+              mode="widthFix" />
+            <image v-else src="/static/images/vector3.png" alt="" mode="widthFix" />
           </view>
           <text class="status-title">{{ getOrderStatusText(orderDetail.orderStatus) }}</text>
-          <text class="status-subtitle">{{ orderDetail.statusMessage }}</text>
+          <text v-if="approveDesc" class="success-text">{{ approveDesc }}</text>
         </view>
 
         <!-- Approval Progress -->
-        <view class="section-card">
+        <!-- <view class="section-card">
           <view class="section-header">
             <uni-icons type="bars" size="18" color="#2563eb" />
             <text class="section-title">{{ t('card.Status.t15') }}</text>
@@ -54,13 +52,13 @@
               </view>
             </view>
           </view>
-        </view>
+        </view> -->
 
         <!-- Amount Info -->
         <view class="section-card">
           <view class="section-header">
             <uni-icons type="wallet" size="18" color="#2563eb" />
-            <text class="section-title">{{ t('card.Form.f37') }}</text>
+            <text class="section-title">{{ t('card.Form.f55') }}</text>
           </view>
 
           <view class="info-list">
@@ -94,14 +92,14 @@
         <view class="section-card">
           <view class="section-header">
             <uni-icons type="list" size="18" color="#2563eb" />
-            <text class="section-title">{{ t('card.Status.t15') }}</text>
+            <text class="section-title">{{ t('global.title3') }}</text>
           </view>
 
           <view class="info-list">
             <view class="info-row" v-if="orderDetail.orderNo">
               <text class="info-label">{{ t('card.Form.f35') }}</text>
               <view class="info-value-wrapper">
-                <text class="info-value">{{ orderDetail.orderNo }}</text>
+                <text class="info-value">{{ formatOrderNo(orderDetail.orderNo) }}</text>
                 <cwg-icon name="copy" :size="14" color="#9ca3af" @click.stop="copyOrderNo" />
               </view>
             </view>
@@ -137,53 +135,18 @@
             </view>
           </view>
         </view>
-
-        <!-- 
-      <view class="service-card" @click="contactService">
-        <uni-icons type="headphones" size="20" color="#2563eb" />
-        <text class="service-text">联系客服</text>
-        <uni-icons type="right" size="16" color="#9ca3af" />
-      </view>
-      Contact Service -->
-      </view>
-
-      <!-- 
-    <view v-if="showActions" class="bottom-actions">
-      <view 
-        v-if="orderDetail.orderStatus === 'processing'" 
-        class="action-btn cancel-btn" 
-        @click="cancelOrder"
-      >
-        <text class="btn-text">取消订单</text>
-      </view>
-      
-      <view 
-        v-if="orderDetail.orderStatus === 'failed'" 
-        class="action-btn appeal-btn" 
-        @click="appealOrder"
-      >
-        <text class="btn-text">申诉</text>
       </view>
-      
-      <view 
-        v-if="orderDetail.orderStatus === 'success'" 
-        class="action-btn delete-btn" 
-        @click="deleteOrder"
-      >
-        <text class="btn-text">删除订单</text>
-      </view>
-    </view>
-    Bottom Actions -->
     </view>
   </cwg-page-wrapper>
 </template>
 
 <script setup lang="ts">
-import { reactive, computed } from 'vue';
+import { reactive, computed, ref } from 'vue';
 import { onLoad, onUnload } from '@dcloudio/uni-app';
 import { useI18n } from 'vue-i18n';
 import useCardStore from '@/stores/use-card-store';
-
+import useUserStore from '@/stores/use-user-store';
+import { ucardApi } from '@/api/ucard';
 type OrderStatus = 'success' | 'processing' | 'failed' | 'cancelled';
 type ApprovalStatus = 'completed' | 'current' | 'pending';
 
@@ -232,34 +195,49 @@ const orderDetail = reactive<OrderDetail>({
   approvalSteps: []
 });
 
-const { t } = useI18n();
+const { t, locale } = useI18n();
 const cardStore = useCardStore();
-
-const headerTitle = computed(() => {
-  // 统一使用交易详情的多语言标题
-  return t('card.title');
-});
-
-const showActions = computed(() => {
-  return ['processing', 'failed', 'success'].includes(orderDetail.orderStatus);
-});
-
-// 获取订单状态图标
-const getOrderStatusIcon = (status: OrderStatus): string => {
-  switch (status) {
-    case 'success':
-      return 'checkmarkempty';
-    case 'processing':
-      return 'loop';
-    case 'failed':
-      return 'closeempty';
-    case 'cancelled':
-      return 'closeempty';
-    default:
-      return 'info';
+const userStore = useUserStore();
+const approveDesc = ref('');
+const getApproveDesc = () => {
+  const d = orderDetail.approveDesc
+  if (!d) return
+  const c = userStore.reasonsOptions
+  const a = c[d || '']
+  const b = locale.value == 'cn' || locale.value == 'zh' ? a.content : a.enContent
+  if (!b) {
+    reasonsRefusalList()
   }
-};
-
+  approveDesc.value = b;
+}
+async function reasonsRefusalList() {
+  try {
+    const res = await ucardApi.reasonsRefusalList();
+    if (res.code === 200) {
+      pickFields(res.data);
+      getApproveDesc()
+    } else {
+      uni.$u.toast(res.msg || t("login.msg0"));
+    }
+  } catch (error) {
+    console.log(error, 111);
+  }
+}
+function pickFields(source, fields = ['content', 'enContent']) {
+  const result = {}
+  Object.entries(source).forEach(([key, value]) => {
+    result[key] = fields.reduce((acc, f) => {
+      acc[f] = value[f] ?? null
+      return acc
+    }, {})
+  })
+  userStore.saveReasonsOptions(result);
+}
+const formatOrderNo = (orderNo?: string) => {
+  if (!orderNo) return '--';
+  if (orderNo.length <= 20) return orderNo;
+  return orderNo.slice(0, 6) + '...' + orderNo.slice(-4);
+}
 // 获取订单状态颜色
 const getOrderStatusColor = (status: OrderStatus): string => {
   switch (status) {
@@ -292,11 +270,6 @@ const getOrderStatusText = (status: OrderStatus): string => {
   }
 };
 
-// 返回
-const goBack = () => {
-  uni.navigateBack();
-};
-
 // 复制订单号
 const copyOrderNo = () => {
   uni.setClipboardData({

+ 2 - 23
pages/wallet/components/GlobalList.vue

@@ -1,8 +1,8 @@
 <template>
     <cwg-load-more-wrapper ref="loadMoreWrapperRef" :loading="loading" :finished="finished" :height='54'
         :refresher-enabled="true" @reach-bottom="loadMore" @refresh="handleRefresh">
-        <view v-if="filteredRecords.length > 0" class="records-list">
-            <view v-for="record in filteredRecords" :key="record.id" class="record-card"
+        <view v-if="records.length > 0" class="records-list">
+            <view v-for="record in records" :key="record.id" class="record-card"
                 @click="goToDeductionDetail(record)">
                 <view class="record-main">
                     <view class="record-left">
@@ -267,27 +267,6 @@ const loadMore = () => {
     fetchRecords(true);
 };
 
-const filteredRecords = computed(() => {
-    return records.value.filter(record => {
-        const type = record.typeStr || record.type;
-        if (props.typeIndex > 0 && type !== props.typeOptions[props.typeIndex]) {
-            return false;
-        }
-        const statusValue = getStatusValue(props.statusIndex);
-        if (statusValue && normalizeStatus(record.status) !== statusValue) {
-            return false;
-        }
-        if (props.dateFilter) {
-            const time = record.addTime || record.time;
-            const datePart = getDatePart(time);
-            if (!datePart || datePart !== props.dateFilter) {
-                return false;
-            }
-        }
-        return true;
-    });
-});
-
 watch([() => props.dateFilter], () => {
     page.value = 1;
     finished.value = false;

+ 482 - 0
pages/wallet/components/VaultodyList.vue

@@ -0,0 +1,482 @@
+<template>
+    <cwg-load-more-wrapper ref="loadMoreWrapperRef" :loading="loading" :finished="finished" :height='54'
+        :refresher-enabled="true" @reach-bottom="loadMore" @refresh="handleRefresh">
+        <view v-if="records.length > 0" class="records-list">
+            <view v-for="record in records" :key="record.id" class="record-card" @click="goToDeductionDetail(record)">
+                <view class="record-main">
+                    <view class="record-left">
+                        <view class="type-icon deduction-icon">
+                            <cwg-icon class="icons" :name="getDeductionIcon(record.type || record.typeStr)" :size="20"
+                                color="#ef4444" />
+                        </view>
+                    </view>
+
+                    <view class="record-right">
+                        <view class="record-info">
+                            <view class="info-header">
+                                <text class="record-type">{{ Math.abs(Number(record.receivedAmount ||
+                                    0)) }}
+                                    {{
+                                        record.receivedCurrency
+                                        || 'USD' }}</text>
+                                <text class="record-detail"><cwg-icon name="icon_transfer" :size="18"
+                                        color="#000" /></text>
+                                <text class="record-type">{{ Math.abs(Number(record.amount || 0)).toFixed(2) }}
+                                    {{
+                                        record.currency
+                                        || 'USDT' }}</text>
+
+                                <view :class="['status-badge', getStatusBadgeClass(record.status)]">
+                                    <cwg-icon class="icons" :name="getStatusIcon(record.status)" :size="12"
+                                        :color="getStatusColor(record.status)" />
+                                    <text :class="['status-text', getStatusTextClass(record.status)]">
+                                        {{ getStatusText(record.status) }}
+                                    </text>
+                                </view>
+                            </view>
+                        </view>
+                    </view>
+                </view>
+
+                <view class="record-footer">
+                    <text class="footer-time">{{ formatDateTime(record.addTime || record.time) }}</text>
+                    <view class="footer-actions">
+                        <text class="footer-order">
+                            {{ t('DepositAddress.p2') }}: {{ formatOrderNo(record.address) }}
+                        </text>
+                        <cwg-icon class="footer-order-icon" name="copy" :size="14" color="#9ca3af"
+                            @click.stop="copyOrderNo(record.address)" />
+                    </view>
+                </view>
+            </view>
+        </view>
+        <cwg-empty-state v-else :text="t('empty-state.c2')" />
+    </cwg-load-more-wrapper>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch, onMounted } from 'vue';
+import dayjs from 'dayjs';
+import { useI18n } from 'vue-i18n';
+import { showToast } from '@/utils/toast';
+import { ucardApi, TransactionInfo } from '@/api/ucard';
+import useRouter from "@/hooks/useRouter";
+const router = useRouter();
+import { vaultodyStatusText } from '@/utils/dataMap';
+import useCardStore from '@/stores/use-card-store';
+
+interface RecordItem extends TransactionInfo {
+    type?: string | number;
+    typeStr?: string;
+    remark?: string;
+    status?: string | number;
+    addTime?: string | number;
+    fee?: number;
+    currency?: string;
+    merchantOrderNo?: string;
+    reason?: string;
+    time?: string | number;
+}
+
+type NormalizedStatus = 'success' | 'wait_process' | 'fail';
+
+const props = defineProps<{
+    cardNo: string;
+    typeIndex: number;
+    statusIndex: number;
+    dateFilter: string;
+    typeOptions: string[];
+    payoutCurrency: string;
+}>();
+
+const { t } = useI18n();
+
+const records = ref<RecordItem[]>([]);
+const page = ref(1);
+const pageSize = 10;
+const loading = ref(false);
+const finished = ref(false);
+
+const cardStore = useCardStore();
+const normalizeStatus = (status?: string | number): NormalizedStatus => {
+    if (status == 2) return 'fail';
+    if (status == '1') return 'success';
+    return 'success';
+};
+
+const getStatusText = (status?: string | number): string => {
+    return t(vaultodyStatusText[status]);
+};
+
+const getStatusIcon = (status?: string | number): string => {
+    const normalized = normalizeStatus(status);
+    if (normalized === 'success') return 'checkmarkempty1';
+    if (normalized === 'wait_process') return 'info1';
+    return 'closeempty1';
+};
+
+const getStatusColor = (status?: string | number): string => {
+    const normalized = normalizeStatus(status);
+    if (normalized === 'success') return '#22c55e';
+    if (normalized === 'wait_process') return '#eab308';
+    return '#ef4444';
+};
+
+const getStatusBadgeClass = (status?: string | number) => `status-${normalizeStatus(status)}`;
+const getStatusTextClass = (status?: string | number) => `status-text-${normalizeStatus(status)}`;
+
+const getStatusValue = (index: number): NormalizedStatus | null => {
+    if (index === 0) return null;
+    const statusMap: NormalizedStatus[] = ['success', 'wait_process', 'fail'];
+    return statusMap[index - 1];
+};
+
+const getDeductionIcon = (type?: string | number): string => {
+    const typeStr = String(type || '');
+    if (typeStr.includes('服务费') || typeStr === '1') return 'servicefee';
+    if (typeStr.includes('手续费') || typeStr === '2') return 'handlingfee';
+    return 'handlingfee';
+};
+
+const formatDateTime = (time?: string | number): string => {
+    if (!time) return '--';
+    try {
+        let date: dayjs.Dayjs;
+        if (typeof time === 'number') {
+            if (time.toString().length === 10) {
+                date = dayjs.unix(time);
+            } else {
+                date = dayjs(time);
+            }
+        } else {
+            date = dayjs(time);
+        }
+        if (!date.isValid()) return '--';
+        return date.format('YYYY-MM-DD HH:mm:ss');
+    } catch (error) {
+        return '--';
+    }
+};
+
+const getDatePart = (time?: string | number): string => {
+    if (!time) return '';
+    try {
+        let date: dayjs.Dayjs;
+        if (typeof time === 'number') {
+            if (time.toString().length === 10) {
+                date = dayjs.unix(time);
+            } else {
+                date = dayjs(time);
+            }
+        } else {
+            date = dayjs(time);
+        }
+        if (!date.isValid()) return '';
+        return date.format('YYYY-MM-DD');
+    } catch (error) {
+        return '';
+    }
+};
+
+const formatOrderNo = (orderNo?: string) => {
+    if (!orderNo) return '--';
+    if (orderNo.length <= 20) return orderNo;
+    return orderNo.slice(0, 6) + '...' + orderNo.slice(-4);
+};
+
+const copyOrderNo = (orderNo?: string) => {
+    if (!orderNo) return;
+    uni.setClipboardData({
+        data: orderNo,
+        success: () => {
+            uni.showToast({
+                title: t('card.Msg.m8') || '复制成功',
+                icon: 'success'
+            });
+        }
+    });
+};
+
+const fetchRecords = async (isLoadMore = false) => {
+    if (isLoadMore && finished.value) return;
+    loading.value = true;
+    try {
+        const res = await ucardApi.getBlockchainTransactionPage({
+            cardNo: props.cardNo,
+            payoutCurrency: props.payoutCurrency,
+            status: props.statusIndex == 0 ? undefined : props.statusIndex,
+            beginDate: props.dateFilter ? dayjs(props.dateFilter).format('YYYY-MM-DD') : undefined,
+            endDate: props.dateFilter ? dayjs(props.dateFilter).format('YYYY-MM-DD') : undefined,
+            page: { current: page.value, row: pageSize },
+        });
+        const data = res.code === 200 && Array.isArray(res.data) ? res.data : [];
+
+        if (isLoadMore) {
+            records.value.push(...data);
+        } else {
+            records.value = data;
+        }
+        console.log(records.value, 1112212);
+
+
+        if (data.length < pageSize) {
+            finished.value = true;
+        } else {
+            finished.value = false;
+        }
+    } catch (error: any) {
+        console.log(error, 11111);
+
+        if (!isLoadMore) {
+            records.value = [];
+        }
+        showToast(error?.message || String(error));
+    } finally {
+        loading.value = false;
+    }
+};
+
+const goToDeductionDetail = (record: RecordItem) => {
+    cardStore.saveOrderDetail(record);
+    router.push({
+        path: '/pages/wallet/global-detail',
+        query: { id: record.id }
+    });
+};
+
+const loadMore = () => {
+    if (finished.value || loading.value) return;
+    page.value++;
+    fetchRecords(true);
+};
+
+watch([() => props.dateFilter], () => {
+    page.value = 1;
+    finished.value = false;
+    fetchRecords();
+}, { immediate: false });
+watch([() => props.payoutCurrency], () => {
+    page.value = 1;
+    finished.value = false;
+    fetchRecords();
+}, { immediate: false });
+watch([() => props.statusIndex], () => {
+    page.value = 1;
+    finished.value = false;
+    fetchRecords();
+}, { immediate: false });
+
+const loadMoreWrapperRef = ref<any>(null);
+
+const refresh = async () => {
+    page.value = 1;
+    finished.value = false;
+    await fetchRecords();
+};
+
+const handleRefresh = async () => {
+    await refresh();
+    // 停止下拉刷新动画
+    if (loadMoreWrapperRef.value) {
+        loadMoreWrapperRef.value.stopRefresh();
+    }
+};
+
+onMounted(() => {
+    fetchRecords();
+});
+
+defineExpose({
+    refresh
+});
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.records-list {
+    display: flex;
+    flex-direction: column;
+    gap: px2rpx(12);
+    padding: px2rpx(16);
+}
+
+.record-card {
+    background-color: #ffffff;
+    border-radius: px2rpx(12);
+    border: 1px solid #e5e7eb;
+    overflow: hidden;
+    transition: box-shadow 0.3s;
+    padding: px2rpx(16);
+    position: relative;
+}
+
+.record-card:active {
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+}
+
+.record-main {
+    display: flex;
+    align-items: flex-start;
+    justify-content: space-between;
+    padding-bottom: px2rpx(16);
+}
+
+.record-left {
+    display: flex;
+    align-items: flex-start;
+    gap: px2rpx(12);
+    min-width: 0;
+}
+
+.type-icon {
+    width: px2rpx(40);
+    height: px2rpx(40);
+    border-radius: px2rpx(10);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+}
+
+.deduction-icon {
+    background-color: #fef2f2;
+}
+
+.icons {
+    width: px2rpx(20);
+    height: px2rpx(20);
+}
+
+.record-info {
+    flex: 1;
+    min-width: 0;
+    display: flex;
+    flex-direction: column;
+    gap: px2rpx(6);
+}
+
+.info-header {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(4);
+    flex-wrap: wrap;
+}
+
+.record-type {
+    font-size: px2rpx(15);
+    color: #111827;
+}
+
+.status-badge {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(4);
+    padding: px2rpx(3) px2rpx(8) px2rpx(3) px2rpx(3);
+    border-radius: px2rpx(12);
+    position: absolute;
+    top: px2rpx(1);
+    right: px2rpx(1);
+}
+
+.status-success {
+    background-color: #f0fdf4;
+}
+
+.status-wait_process {
+    background-color: #fefce8;
+}
+
+.status-fail {
+    background-color: #fef2f2;
+}
+
+.status-text {
+    font-size: px2rpx(11);
+}
+
+.status-text-success {
+    color: #22c55e;
+}
+
+.status-text-wait_process {
+    color: #eab308;
+}
+
+.status-text-fail {
+    color: #ef4444;
+}
+
+.record-detail {
+    font-size: px2rpx(13);
+    color: #6b7280;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.record-right {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: px2rpx(4);
+    margin-left: px2rpx(12);
+    flex-shrink: 0;
+
+    .row {
+        width: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: space-around;
+
+    }
+
+    .l {
+        flex: 1;
+    }
+
+    .r {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        gap: px2rpx(4);
+        margin-left: px2rpx(12);
+        flex-shrink: 0;
+    }
+}
+
+.amount-deduction {
+    font-size: px2rpx(18);
+    color: #ef4444;
+}
+
+.fee-text {
+    font-size: px2rpx(11);
+    color: #9ca3af;
+}
+
+.record-footer {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding-top: px2rpx(16);
+    border-top: 1px solid #f3f4f6;
+}
+
+.footer-time {
+    font-size: px2rpx(11);
+    color: #9ca3af;
+}
+
+.footer-actions {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(2);
+}
+
+.footer-detail {
+    font-size: px2rpx(11);
+    color: #2563eb;
+}
+</style>

+ 477 - 0
pages/wallet/components/WithdrawList.vue

@@ -0,0 +1,477 @@
+<template>
+    <cwg-load-more-wrapper ref="loadMoreWrapperRef" :loading="loading" :finished="finished" :height='54'
+        :refresher-enabled="true" @reach-bottom="loadMore" @refresh="handleRefresh">
+        <view v-if="records.length > 0" class="records-list">
+            <view v-for="record in records" :key="record.id" class="record-card" @click="goToDeductionDetail(record)">
+                <view class="record-main">
+                    <view class="record-left">
+                        <view class="type-icon deduction-icon">
+                            <cwg-icon class="icons" :name="getDeductionIcon(record.type || record.typeStr)" :size="20"
+                                color="#ef4444" />
+                        </view>
+                    </view>
+
+                    <view class="record-right">
+                        <view class="record-info">
+                            <view class="info-header">
+                                <text class="record-type">{{ Math.abs(Number(record.amount || 0)) }}
+                                    {{ record.currency || 'USD' }}</text>
+                                <text class="record-detail"><cwg-icon name="icon_transfer" :size="18"
+                                        color="#000" /></text>
+                                <text class="record-type">{{ Math.abs(Number(record.receivedAmount || 0)) }}
+                                    {{ record.receivedCurrency }}</text>
+                                <view :class="['status-badge', getStatusBadgeClass(record.transactionStatus)]">
+                                    <cwg-icon class="icons" :name="getStatusIcon(record.transactionStatus)" :size="12"
+                                        :color="getStatusColor(record.transactionStatus)" />
+                                    <text :class="['status-text', getStatusTextClass(record.transactionStatus)]">
+                                        {{ getStatusText(record.transactionStatus) }}
+                                    </text>
+                                </view>
+                            </view>
+                        </view>
+                    </view>
+                </view>
+
+                <view class="record-footer">
+                    <text class="footer-time">{{ formatDateTime(record.addTime || record.time) }}</text>
+                    <view class="footer-actions">
+                        <text class="footer-order">
+                            {{ t('DepositAddress.p2') }}: {{ formatOrderNo(record.address) }}
+                        </text>
+                        <cwg-icon class="footer-order-icon" name="copy" :size="14" color="#9ca3af"
+                            @click.stop="copyOrderNo(record.address)" />
+                    </view>
+                </view>
+            </view>
+        </view>
+        <cwg-empty-state v-else :text="t('empty-state.c2')" />
+    </cwg-load-more-wrapper>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch, onMounted } from 'vue';
+import dayjs from 'dayjs';
+import { useI18n } from 'vue-i18n';
+import { showToast } from '@/utils/toast';
+import { ucardApi, TransactionInfo } from '@/api/ucard';
+import useRouter from "@/hooks/useRouter";
+const router = useRouter();
+import { withdrawStatus } from '@/utils/dataMap';
+import useCardStore from '@/stores/use-card-store';
+
+interface RecordItem extends TransactionInfo {
+    type?: string | number;
+    typeStr?: string;
+    remark?: string;
+    status?: string | number;
+    addTime?: string | number;
+    fee?: number;
+    currency?: string;
+    merchantOrderNo?: string;
+    reason?: string;
+    time?: string | number;
+}
+
+type NormalizedStatus = 'success' | 'wait_process' | 'fail';
+
+const props = defineProps<{
+    cardNo: string;
+    typeIndex: number;
+    statusIndex: number;
+    dateFilter: string;
+    typeOptions: string[];
+    payoutCurrency: string;
+}>();
+
+const { t } = useI18n();
+
+const records = ref<RecordItem[]>([]);
+const page = ref(1);
+const pageSize = 10;
+const loading = ref(false);
+const finished = ref(false);
+
+const cardStore = useCardStore();
+const normalizeStatus = (status?: string | number): NormalizedStatus => {
+    if (!status) return 'wait_process';
+    if (status == 4 || status == 5) return 'fail';
+    if (status == 3) return 'success';
+    return 'wait_process';
+};
+
+const getStatusText = (status?: string | number): string => {
+    return t(withdrawStatus[status]);
+};
+
+const getStatusIcon = (status?: string | number): string => {
+    const normalized = normalizeStatus(status);
+    if (normalized === 'success') return 'checkmarkempty1';
+    if (normalized === 'wait_process') return 'info1';
+    return 'closeempty1';
+};
+
+const getStatusColor = (status?: string | number): string => {
+    const normalized = normalizeStatus(status);
+    if (normalized === 'success') return '#22c55e';
+    if (normalized === 'wait_process') return '#eab308';
+    return '#ef4444';
+};
+
+const getStatusBadgeClass = (status?: string | number) => `status-${normalizeStatus(status)}`;
+const getStatusTextClass = (status?: string | number) => `status-text-${normalizeStatus(status)}`;
+
+const getStatusValue = (index: number): NormalizedStatus | null => {
+    if (index === 0) return null;
+    const statusMap: NormalizedStatus[] = ['success', 'wait_process', 'fail'];
+    return statusMap[index - 1];
+};
+
+const getDeductionIcon = (type?: string | number): string => {
+    const typeStr = String(type || '');
+    if (typeStr.includes('服务费') || typeStr === '1') return 'servicefee';
+    if (typeStr.includes('手续费') || typeStr === '2') return 'handlingfee';
+    return 'handlingfee';
+};
+
+const formatDateTime = (time?: string | number): string => {
+    if (!time) return '--';
+    try {
+        let date: dayjs.Dayjs;
+        if (typeof time === 'number') {
+            if (time.toString().length === 10) {
+                date = dayjs.unix(time);
+            } else {
+                date = dayjs(time);
+            }
+        } else {
+            date = dayjs(time);
+        }
+        if (!date.isValid()) return '--';
+        return date.format('YYYY-MM-DD HH:mm:ss');
+    } catch (error) {
+        return '--';
+    }
+};
+
+const getDatePart = (time?: string | number): string => {
+    if (!time) return '';
+    try {
+        let date: dayjs.Dayjs;
+        if (typeof time === 'number') {
+            if (time.toString().length === 10) {
+                date = dayjs.unix(time);
+            } else {
+                date = dayjs(time);
+            }
+        } else {
+            date = dayjs(time);
+        }
+        if (!date.isValid()) return '';
+        return date.format('YYYY-MM-DD');
+    } catch (error) {
+        return '';
+    }
+};
+
+const formatOrderNo = (orderNo?: string) => {
+    if (!orderNo) return '--';
+    if (orderNo.length <= 20) return orderNo;
+    return orderNo.slice(0, 6) + '...' + orderNo.slice(-4);
+};
+
+const copyOrderNo = (orderNo?: string) => {
+    if (!orderNo) return;
+    uni.setClipboardData({
+        data: orderNo,
+        success: () => {
+            uni.showToast({
+                title: t('card.Msg.m8') || '复制成功',
+                icon: 'success'
+            });
+        }
+    });
+};
+
+const fetchRecords = async (isLoadMore = false) => {
+    if (isLoadMore && finished.value) return;
+    loading.value = true;
+    try {
+        const res = await ucardApi.getBlockchainWithdrawPage({
+            cardNo: props.cardNo,
+            payoutCurrency: props.payoutCurrency,
+            transactionStatus: props.statusIndex == 0 ? undefined : props.statusIndex,
+            beginDate: props.dateFilter ? dayjs(props.dateFilter).format('YYYY-MM-DD') : undefined,
+            endDate: props.dateFilter ? dayjs(props.dateFilter).format('YYYY-MM-DD') : undefined,
+            page: { current: page.value, row: pageSize },
+        });
+        const data = res.code === 200 && Array.isArray(res.data) ? res.data : [];
+
+        if (isLoadMore) {
+            records.value.push(...data);
+        } else {
+            records.value = data;
+        }
+        console.log(records.value, 1112212);
+
+
+        if (data.length < pageSize) {
+            finished.value = true;
+        } else {
+            finished.value = false;
+        }
+    } catch (error: any) {
+        console.log(error, 11111);
+
+        if (!isLoadMore) {
+            records.value = [];
+        }
+        showToast(error?.message || String(error));
+    } finally {
+        loading.value = false;
+    }
+};
+
+const goToDeductionDetail = (record: RecordItem) => {
+    cardStore.saveOrderDetail(record);
+    router.push({
+        path: '/pages/wallet/withdraw-detail',
+        query: { id: record.id }
+    });
+};
+
+const loadMore = () => {
+    if (finished.value || loading.value) return;
+    page.value++;
+    fetchRecords(true);
+};
+
+watch([() => props.dateFilter], () => {
+    page.value = 1;
+    finished.value = false;
+    fetchRecords();
+}, { immediate: false });
+watch([() => props.payoutCurrency], () => {
+    page.value = 1;
+    finished.value = false;
+    fetchRecords();
+}, { immediate: false });
+watch([() => props.statusIndex], () => {
+    page.value = 1;
+    finished.value = false;
+    fetchRecords();
+}, { immediate: false });
+
+const loadMoreWrapperRef = ref<any>(null);
+
+const refresh = async () => {
+    page.value = 1;
+    finished.value = false;
+    await fetchRecords();
+};
+
+const handleRefresh = async () => {
+    await refresh();
+    // 停止下拉刷新动画
+    if (loadMoreWrapperRef.value) {
+        loadMoreWrapperRef.value.stopRefresh();
+    }
+};
+
+onMounted(() => {
+    fetchRecords();
+});
+
+defineExpose({
+    refresh
+});
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.records-list {
+    display: flex;
+    flex-direction: column;
+    gap: px2rpx(12);
+    padding: px2rpx(16);
+}
+
+.record-card {
+    background-color: #ffffff;
+    border-radius: px2rpx(12);
+    border: 1px solid #e5e7eb;
+    overflow: hidden;
+    transition: box-shadow 0.3s;
+    padding: px2rpx(16);
+    position: relative;
+}
+
+.record-card:active {
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+}
+
+.record-main {
+    display: flex;
+    align-items: flex-start;
+    justify-content: space-between;
+    padding-bottom: px2rpx(16);
+}
+
+.record-left {
+    display: flex;
+    align-items: flex-start;
+    gap: px2rpx(12);
+    min-width: 0;
+}
+
+.type-icon {
+    width: px2rpx(40);
+    height: px2rpx(40);
+    border-radius: px2rpx(10);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+}
+
+.deduction-icon {
+    background-color: #fef2f2;
+}
+
+.icons {
+    width: px2rpx(20);
+    height: px2rpx(20);
+}
+
+.record-info {
+    flex: 1;
+    min-width: 0;
+    display: flex;
+    flex-direction: column;
+    gap: px2rpx(6);
+}
+
+.info-header {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(4);
+    flex-wrap: wrap;
+}
+
+.record-type {
+    font-size: px2rpx(15);
+    color: #111827;
+}
+
+.status-badge {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(4);
+    padding: px2rpx(3) px2rpx(8) px2rpx(3) px2rpx(3);
+    border-radius: px2rpx(12);
+    position: absolute;
+    top: px2rpx(1);
+    right: px2rpx(1);
+}
+
+.status-success {
+    background-color: #f0fdf4;
+}
+
+.status-wait_process {
+    background-color: #fefce8;
+}
+
+.status-fail {
+    background-color: #fef2f2;
+}
+
+.status-text {
+    font-size: px2rpx(11);
+}
+
+.status-text-success {
+    color: #22c55e;
+}
+
+.status-text-wait_process {
+    color: #eab308;
+}
+
+.status-text-fail {
+    color: #ef4444;
+}
+
+.record-detail {
+    font-size: px2rpx(13);
+    color: #6b7280;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.record-right {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: px2rpx(4);
+    margin-left: px2rpx(12);
+    flex-shrink: 0;
+
+    .row {
+        width: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: space-around;
+
+    }
+
+    .l {
+        flex: 1;
+    }
+
+    .r {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        gap: px2rpx(4);
+        margin-left: px2rpx(12);
+        flex-shrink: 0;
+    }
+}
+
+.amount-deduction {
+    font-size: px2rpx(18);
+    color: #ef4444;
+}
+
+.fee-text {
+    font-size: px2rpx(11);
+    color: #9ca3af;
+}
+
+.record-footer {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding-top: px2rpx(16);
+    border-top: 1px solid #f3f4f6;
+}
+
+.footer-time {
+    font-size: px2rpx(11);
+    color: #9ca3af;
+}
+
+.footer-actions {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(2);
+}
+
+.footer-detail {
+    font-size: px2rpx(11);
+    color: #2563eb;
+}
+</style>

+ 12 - 7
pages/wallet/global-detail.vue

@@ -1,15 +1,20 @@
 <template>
   <cwg-page-wrapper>
     <view class="order-detail-page">
-      <view class="success-icon-wrap">
-        <image v-if="detailData.status === 'success'" src="/static/images/vector.png" alt="" mode="widthFix" />
-        <image v-else-if="detailData.status === 'fail'" src="/static/images/vector2.png" alt="" mode="widthFix" />
-        <image v-else src="/static/images/vector3.png" alt="" mode="widthFix" />
-        <text class="success-text">{{ t(globalStatusText[detailData.status]) }}</text>
-        <text v-if="approveDesc" class="success-text">{{ approveDesc }}</text>
-      </view>
+
+
       <!-- Content -->
       <view class="content">
+        <view class="section-card">
+          <view class="success-icon-wrap">
+            <image v-if="detailData.status === 'success'" src="/static/images/vector.png" alt="" mode="widthFix" />
+            <image v-else-if="detailData.status === 'fail'" src="/static/images/vector2.png" alt="" mode="widthFix" />
+            <image v-else src="/static/images/vector3.png" alt="" mode="widthFix" />
+            <text class="success-text">{{ t(globalStatusText[detailData.status]) }}</text>
+            <text v-if="approveDesc" class="success-text">{{ approveDesc }}</text>
+          </view>
+        </view>
+
         <view class="section-card">
           <view class="section-header">
             <uni-icons type="wallet" size="18" color="#2563eb" />

+ 30 - 5
pages/wallet/index.vue

@@ -17,6 +17,13 @@
           </view>
         </view>
       </view>
+
+      <view class="wallet-actions">
+        <view class="cwg-button two-btn">
+          <u-button type="primary" block @click="goToVaultody">{{ t('pages.wallet.vaultody') }}</u-button>
+          <u-button plain block class="prev-btn" @click="goToWithdraw">{{ t('pages.wallet.withdraw') }}</u-button>
+        </view>
+      </view>
       <view class="wallet-page">
         <view class="global-title">{{ t("wallet1.p3") }}</view>
         <view class="global-con">
@@ -38,14 +45,14 @@
         <view class="cwg-button">
           <u-button type="primary" block @click="goToGlobalRemit">{{
             t("global.title1")
-          }}</u-button>
+            }}</u-button>
         </view>
         <view class="trans-header">
           <view class="record-title">{{ t("global.title") }} </view>
 
           <view class="all" @click="goRechargeRecord">{{
             t("card.Status.t22")
-          }}</view>
+            }}</view>
 
         </view>
 
@@ -101,10 +108,8 @@ const currency = ref("EUR");
 const balance = ref("USD");
 const modelValue = ref(false);
 const modelValue1 = ref(false);
-const isShow = ref(false);
 const currencyList = ref([]);
 const allAmount = ref(0);
-const bankConfigList = ref([]);
 async function getBalance() {
   try {
     const res = await ucardApi.walletBalance();
@@ -180,6 +185,12 @@ function goToGlobalRemit() {
     `/pages/wallet/global-order?currency=${currency.value}`
   );
 }
+function goToVaultody() {
+  router.push(`/pages/wallet/vaultody`);
+}
+function goToWithdraw() {
+  router.push(`/pages/wallet/withdraw?allAmount=${allAmount.value}`);
+}
 function changeSelect(e) {
   currency.value = e.value;
 }
@@ -194,7 +205,6 @@ const getGlobalCurrenciesDropdown = async () => {
       return { currency: i.payoutCurrency, text: i.country, value: i.payoutCurrency };
     }) || [];
     const data = _.cloneDeep(res.data);
-
     cardStore.saveCurrencyList(data)
   }
 };
@@ -469,4 +479,19 @@ onMounted(() => {
     color: #ef4444;
   }
 }
+
+.wallet-actions {
+  padding: px2rpx(16) px2rpx(24);
+}
+
+.wallet-actions .two-btn {
+  display: flex;
+  gap: px2rpx(16);
+}
+
+.wallet-actions .prev-btn {
+  border: 1px solid var(--main-yellow) !important;
+  color: #fff !important;
+  background: transparent;
+}
 </style>

+ 277 - 0
pages/wallet/vaultody-list.vue

@@ -0,0 +1,277 @@
+<template>
+    <cwg-page-wrapper>
+        <view class="bank-transaction-page">
+            <view class="filters-container" :style="{ top: statusBarHeight + 53 + 'px' }">
+                <view class="filter-item">
+                    <text class="filter-label">{{ t('card.Form.f45') }}</text>
+                    <cwg-picker v-model="statusFilterIndex" :options="statusOptions" />
+                </view>
+                <view class="filter-item">
+                    <text class="filter-label">{{ t('card.Form.f43') }}</text>
+                    <view class="filter-picker" @click="open">
+                        <view class="picker-value">
+                            <text class="picker-text">{{ dateFilter || t('card.Form.f57') }}</text>
+                            <uni-icons type="calendar" size="14" color="#6b7280" class="picker-icon" />
+                        </view>
+                    </view>
+                    <cwg-date-picker v-model:show="show" v-model="dateFilter" mode="date" @confirm="onDateConfirm"
+                        :minDate="minDate" :maxDate="maxDate" />
+                </view>
+                <view class="reset-btn" @click="resetFilters">
+                    <uni-icons type="loop" size="16" color="#2563eb" />
+                </view>
+            </view>
+            <view class="content">
+                <VaultodyList ref="globalListRef" :cardNo="cardNo" :payoutCurrency="payoutCurrency"
+                    :statusIndex="statusFilterIndex" :dateFilter="dateFilter" />
+            </view>
+        </view>
+    </cwg-page-wrapper>
+
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch } from 'vue';
+import { onLoad } from '@dcloudio/uni-app';
+import { useI18n } from 'vue-i18n';
+import useGlobalStore from '@/stores/use-global-store';
+import VaultodyList from './components/VaultodyList.vue';
+import { vaultodyStatusText } from '@/utils/dataMap';
+import useCardStore from "@/stores/use-card-store";
+const cardStore = useCardStore();
+const globalStore = useGlobalStore()
+const statusBarHeight = computed(() => globalStore.statusBarHeight);
+const { t } = useI18n();
+const minDate = ref(new Date(2000, 0, 1).getTime());
+const maxDate = ref(new Date().getTime());
+const cardNo = ref('');
+onLoad((options) => {
+    cardNo.value = options.cardNo || '';
+});
+
+const statusOptions = computed(() => {
+    return vaultodyStatusText
+});
+const payoutCurrency = ref();
+const statusFilterIndex = ref(0);
+const dateFilter = ref('');
+
+const show = ref(false)
+const onDateConfirm = (e: any) => {
+    dateFilter.value = e.formatted;
+};
+function open() {
+    show.value = true
+}
+
+const resetFilters = () => {
+    payoutCurrency.value = '';
+    statusFilterIndex.value = 0;
+    dateFilter.value = '';
+};
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.page-wrapper {
+    padding: 0;
+    border: 0;
+}
+
+.bank-transaction-page {
+    // background-color: #f9fafb;
+}
+
+.wallet-header {
+    background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);
+}
+
+/* Header */
+.header {
+    background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);
+    padding: px2rpx(16);
+    padding-bottom: px2rpx(20);
+}
+
+.header-content {
+    display: flex;
+    flex-direction: column;
+    gap: px2rpx(16);
+}
+
+.header-title {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(8);
+}
+
+.title-text {
+    color: #ffffff;
+    font-size: px2rpx(20);
+}
+
+.stats-container {
+    display: flex;
+    gap: px2rpx(12);
+}
+
+.stat-card {
+    flex: 1;
+    background-color: rgba(255, 255, 255, 0.15);
+    backdrop-filter: blur(px2rpx(10));
+    border-radius: px2rpx(12);
+    padding: px2rpx(12);
+}
+
+.stat-header {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(6);
+    margin-bottom: px2rpx(8);
+}
+
+.icons {
+    width: px2rpx(20);
+    height: px2rpx(20);
+}
+
+.stat-label {
+    font-size: px2rpx(12);
+    color: rgba(255, 255, 255, 0.9);
+}
+
+.stat-value {
+    font-size: px2rpx(20);
+    color: #ffffff;
+    display: block;
+    margin-bottom: px2rpx(4);
+}
+
+.stat-count {
+    font-size: px2rpx(11);
+    color: rgba(255, 255, 255, 0.8);
+}
+
+/* Tabs */
+.tabs-container {
+    background-color: #ffffff;
+    display: flex;
+    border-bottom: 1px solid #e5e7eb;
+    position: sticky;
+    top: 0;
+    z-index: 10;
+}
+
+.tab-item {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: px2rpx(6);
+    padding: px2rpx(16);
+    position: relative;
+    transition: all 0.3s;
+}
+
+.tab-text {
+    font-size: px2rpx(15);
+    color: #9ca3af;
+    transition: color 0.3s;
+}
+
+.tab-text-active {
+    color: #2563eb;
+}
+
+.tab-indicator {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    height: px2rpx(3);
+    background-color: #2563eb;
+    border-radius: px2rpx(3) px2rpx(3) 0 0;
+}
+
+/* Content */
+.content {
+    padding: 0;
+}
+
+.filters-container {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(8);
+    padding: px2rpx(12) px2rpx(16);
+    background-color: #ffffff;
+    position: sticky;
+    top: 0;
+    z-index: 10;
+    overflow-x: auto;
+    -webkit-overflow-scrolling: touch;
+}
+
+.filter-item {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(4);
+    flex-shrink: 0;
+    min-width: 0;
+}
+
+.filter-label {
+    font-size: px2rpx(12);
+    color: #6b7280;
+    white-space: nowrap;
+    flex-shrink: 0;
+}
+
+.filter-picker {
+    background-color: #ffffff;
+    border: 1px solid #e5e7eb;
+    border-radius: px2rpx(8);
+    padding: px2rpx(6) px2rpx(8);
+    display: flex;
+    align-items: center;
+    min-width: px2rpx(60);
+    max-width: px2rpx(120);
+    flex-shrink: 1;
+}
+
+.picker-value {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(4);
+    width: 100%;
+    min-width: 0;
+}
+
+.picker-text {
+    flex: 1;
+    min-width: 0;
+    font-size: px2rpx(12);
+    color: #111827;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.picker-icon {
+    flex-shrink: 0;
+    width: px2rpx(14);
+    height: px2rpx(14);
+}
+
+.reset-btn {
+    background-color: #ffffff;
+    border: 1px solid #e5e7eb;
+    border-radius: px2rpx(8);
+    padding: px2rpx(6) px2rpx(10);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+    min-width: px2rpx(36);
+}
+</style>

+ 185 - 0
pages/wallet/vaultody.vue

@@ -0,0 +1,185 @@
+<template>
+    <cwg-page-wrapper>
+        <view class="page">
+            <u-form ref="formRef" :rules="rules" :model="form" class="payment-form">
+                <cwg-input v-model:value="form.blockchain" fkey="blockchain" type="select" :required="true"
+                    :columns="blockchainList" :label="t('Blockchain.addP2')" rulesKey="blockchain"
+                    @change="handleChange" />
+                <cwg-input v-if="text1" v-model:value="text1" fkey="text1" type="text" :label="t('Blockchain1.p2')"
+                    :readonly="true" :disabled="true" />
+            </u-form>
+
+            <QrCode v-if="text1" :text="text1"></QrCode>
+
+            <view class="fixed-btn">
+                <view v-if="!text1" class="cwg-button ok-button">
+                    <u-button type="primary" block @click="getVaultodyAddress">{{
+                        t("Blockchain1.p3")
+                    }}</u-button>
+                </view>
+                <view v-else class="cwg-button ok-button">
+                    <u-button type="primary" block @click="cardCopy">{{
+                        t("Blockchain1.p4")
+                    }}</u-button>
+                </view>
+            </view>
+        </view>
+    </cwg-page-wrapper>
+</template>
+<script setup lang="ts">
+import { ref, onMounted, computed } from "vue";
+import { showToast } from "@/utils/toast";
+import { useI18n } from "vue-i18n";
+import { ucardApi } from "@/api/ucard";
+import useUserStore from "@/stores/use-user-store";
+import QrCode from "@/components/QrCode.vue";
+import { Validators } from "@/utils/validators";
+const { t } = useI18n();
+const userStore = useUserStore();
+const userInfo = computed(() => userStore.userInfo);
+const formRef = ref();
+const form = ref<{
+    emailCode: string;
+    cardNo?: string;
+    password?: string;
+    country?: string;
+    email?: string;
+}>({
+    blockchain: "",
+});
+const text1 = ref("");
+const blockchainList = ref([]);
+// 表单验证规则
+const rules = {
+    blockchain: [Validators.required(t("Blockchain.addP2"))],
+    address: [Validators.required(t("WalletApply.p2"))],
+    amount: [
+        Validators.required(t("global.validator.v15"))
+    ],
+};
+// 初始化表单
+function initForm() {
+    if (userInfo.value) {
+        const a = { uniqueId: userInfo.value.uniqueId, country: userInfo.value.country, email: userInfo.value.email }
+        if (userInfo.value) {
+            form.value = {
+                ...a,
+            };
+        }
+    }
+}
+
+//生成支付地址
+async function getVaultodyAddress() {
+    try {
+        await formRef.value?.validate();
+        let res = await ucardApi.getVaultodyAddress({
+            ...form.value,
+        });
+        if (res.code == 200) {
+            text1.value = res.data;
+        } else {
+            text1.value = "";
+            showToast(res.msg);
+        }
+    } catch (error) {
+        console.log(error, 1111);
+
+    }
+}
+//获取区块链
+async function getBlockchainDropdown() {
+    let res = await ucardApi.getBlockchainDropdown({
+        ...form.value,
+    });
+    if (res.code == 200) {
+        res.data.map((item: any) => {
+            item.text = item.alias;
+            item.value = item.blockchain;
+        });
+        blockchainList.value = res.data;
+    } else {
+        blockchainList.value = []
+        showToast(res.msg);
+    }
+}
+
+// 表单字段变化
+function handleChange(value: any) {
+    if (value.key === "emailCode") {
+        form.value.emailCode = value.value;
+    }
+}
+
+
+// 复制 CVV
+function cardCopy() {
+    let title = t("card.Msg.m8");
+    uni.setClipboardData({
+        data: text1.value,
+        success: () => {
+            uni.showToast({
+                title,
+            });
+        },
+    });
+}
+onMounted(() => {
+    initForm();
+    getBlockchainDropdown();
+});
+</script>
+
+<style lang="scss" scoped>
+@import "@/uni.scss";
+
+.no-button {
+    width: 100%;
+    margin: px2rpx(12) 0;
+
+    .u-button {
+        background-color: #ffbdc8 !important;
+    }
+}
+
+.ok-button {
+    margin: px2rpx(4) 0;
+    /* background-color: #EA002A; */
+}
+
+
+.code-input-label {
+    font-size: var(--font-size-16);
+    line-height: px2rpx(44);
+    letter-spacing: px2rpx(1);
+    color: #474747;
+}
+
+.code-input-wrapper {
+    position: relative;
+    display: flex;
+    align-items: center;
+}
+
+.code-input {
+    flex: 1;
+}
+
+.get-code-btn {
+    min-width: px2rpx(100);
+    margin-bottom: px2rpx(12);
+    margin-left: px2rpx(10);
+
+    .cwg-button .u-button {
+        border-radius: px2rpx(8);
+    }
+}
+
+.submit-section {
+    margin: px2rpx(20) 0;
+}
+
+.submit-btn {
+    width: 100%;
+}
+</style>

+ 659 - 0
pages/wallet/withdraw-detail.vue

@@ -0,0 +1,659 @@
+<template>
+  <cwg-page-wrapper>
+    <view class="order-detail-page">
+      <view class="content">
+        <view class="section-card">
+          <view class="success-icon-wrap">
+            <image v-if="detailData.transactionStatus == 3" src="/static/images/vector.png" alt="" mode="widthFix" />
+            <image v-else-if="detailData.transactionStatus == 4 || detailData.transactionStatus == 5"
+              src="/static/images/vector2.png" alt="" mode="widthFix" />
+            <image v-else src="/static/images/vector3.png" alt="" mode="widthFix" />
+            <text class="success-text">{{ t(withdrawStatus[detailData.transactionStatus]) }}</text>
+            <text v-if="approveDesc" class="success-text">{{ approveDesc }}</text>
+          </view>
+        </view>
+
+        <view class="section-card">
+          <view class="section-header">
+            <uni-icons type="wallet" size="18" color="#2563eb" />
+            <text class="section-title">{{ t('Ib.Report.Title3') }}</text>
+          </view>
+          <view class="info-list">
+            <view class="info-row" v-if="detailData.merchantOrderNo">
+              <text class="info-label">{{ t('card.Form.f35') }}</text>
+              <view class="info-value-wrapper">
+                <text class="info-value">{{ detailData.merchantOrderNo }}</text>
+                <cwg-icon name="copy" :size="14" color="#9ca3af" @click.stop="copyOrderNo" />
+              </view>
+            </view>
+            <view class="info-row">
+              <text class="info-label">{{ t('WalletApply.p3') }}</text>
+              <text class="info-value amount-highlight">
+                {{ detailData.amount }} USDT
+              </text>
+            </view>
+            <view class="info-row">
+              <text class="info-label">{{ t('card.Form.f31') }}</text>
+              <text class="info-value">
+                {{ detailData.receivedAmount || '0' }}
+                <text class="currency">{{ detailData.receivedCurrency }}</text>
+              </text>
+            </view>
+            <view class="info-row">
+              <text class="info-label">{{ t('WalletApply.p8') }}</text>
+              <text class="info-value">
+                {{ detailData.contractAddress }}
+              </text>
+            </view>
+            <view class="info-row">
+              <text class="info-label">{{ t('WalletApply.p9') }}</text>
+              <text class="info-value">
+                {{ detailData.fromAddress }}
+              </text>
+            </view>
+            <view class="info-row">
+              <text class="info-label">{{ t('WalletApply.p1') }}</text>
+              <text class="info-value">
+                {{ formatOrderNo(detailData.address) }}
+                <cwg-icon class="footer-order-icon" name="copy" :size="14" color="#9ca3af"
+                  @click.stop="copyOrderNo(detailData.address)" />
+              </text>
+            </view>
+            <view class="divider"></view>
+            <view class="info-row">
+              <text class="info-label">{{ t('card.Form.f27') }}</text>
+              <text class="info-value">
+                <text v-if="detailData.remark">{{ detailData.remark }}</text>
+                <text v-else-if="detailData.note">{{ detailData.note }}</text>
+                <text v-else-if="approveDesc">{{ approveDesc }}</text>
+                <text v-else>--</text>
+              </text>
+            </view>
+            <view class="info-row">
+              <text class="info-label">{{ t('Label.ApplyTime') }}</text>
+              <text class="info-value">
+                <text>{{ detailData.addTime }}</text>
+              </text>
+            </view>
+            <view class="info-row" v-if="detailData.approveTime">
+              <text class="info-label">{{ t('WalletApply.p10') }}</text>
+              <text class="info-value">
+                <text>{{ detailData.approveTime }}</text>
+              </text>
+            </view>
+            <view class="info-row">
+              <text class="info-label">{{ t('global.p22') }}</text>
+              <text class="info-value">
+                <text>{{ t(withdrawApprovalText[detailData.status]) }}</text>
+              </text>
+            </view>
+            <view class="info-row">
+              <text class="info-label">{{ t('card.Form.f45') }}</text>
+              <text class="info-value">
+                <text>{{ t(withdrawStatus[detailData.transactionStatus]) }}</text>
+              </text>
+            </view>
+          </view>
+        </view>
+      </view>
+      <view class="content" v-if="complianceStatus">
+        <view>
+          <view class="section-header">
+            <uni-icons type="list" size="18" color="#2563eb" />
+            <text class="section-title">{{ getGroupTitle('submitRfi') }}</text>
+          </view>
+        </view>
+      </view>
+      <view class="bottom-actions" v-if="(detailData.transactionStatus == 1 && detailData.transactionStatus != 5)">
+        <view class="action-btn cancel-btn" @click="cancelOrder">
+          <text class="btn-text">{{ t('global.GlobalOrder.CancelOrder') }}</text>
+        </view>
+      </view>
+    </view>
+  </cwg-page-wrapper>
+</template>
+
+<script setup lang="ts">
+import { ref, watch, nextTick, computed } from 'vue'
+import { onLoad, onUnload } from '@dcloudio/uni-app';
+import { useI18n } from 'vue-i18n';
+import useCardStore from '@/stores/use-card-store';
+import { ucardApi } from "@/api/ucard";
+import { withdrawApprovalText, withdrawStatus } from '@/utils/dataMap';
+import useUserStore from "@/stores/use-user-store";
+const { t, locale } = useI18n();
+
+const userStore = useUserStore();
+const approveDesc = ref('');
+const getApproveDesc = () => {
+  const d = detailData.value.approveDesc
+  if (!d) return
+  const c = userStore.reasonsOptions
+  const a = c[d || '']
+  const b = locale.value == 'cn' || locale.value == 'zh' ? a.content : a.enContent
+  if (!b) {
+    reasonsRefusalList()
+  }
+  approveDesc.value = b;
+}
+const formatOrderNo = (orderNo?: string) => {
+  if (!orderNo) return '--';
+  if (orderNo.length <= 20) return orderNo;
+  return orderNo.slice(0, 6) + '...' + orderNo.slice(-4);
+};
+
+const copyOrderNo = (orderNo?: string) => {
+  if (!orderNo) return;
+  uni.setClipboardData({
+    data: orderNo,
+    success: () => {
+      uni.showToast({
+        title: t('card.Msg.m8') || '复制成功',
+        icon: 'success'
+      });
+    }
+  });
+};
+async function reasonsRefusalList() {
+  try {
+    const res = await ucardApi.reasonsRefusalList();
+    if (res.code === 200) {
+      pickFields(res.data);
+      getApproveDesc()
+    } else {
+      uni.$u.toast(res.msg || t("login.msg0"));
+    }
+  } catch (error) {
+    console.log(error, 111);
+  }
+}
+function pickFields(source, fields = ['content', 'enContent']) {
+  const result = {}
+  Object.entries(source).forEach(([key, value]) => {
+    result[key] = fields.reduce((acc, f) => {
+      acc[f] = value[f] ?? null
+      return acc
+    }, {})
+  })
+  userStore.saveReasonsOptions(result);
+}
+
+const cardStore = useCardStore();
+const getGroupTitle = (type) => {
+  const map = {
+    common: "global.GlobalOrder.common",
+    receiver: "global.GlobalOrder.receiver",
+    sender: "global.GlobalOrder.sender",
+    other: "global.GlobalOrder.other",
+    submitRfi: "global.GlobalOrder.submitRfi",
+  };
+  return t(map[type] || type);
+}
+
+// 取消订单
+const cancelOrder = () => {
+  uni.showModal({
+    title: t('Msg.SystemPrompt'),
+    cancelText: t('common.cancel'),
+    confirmText: t('common.confirm'),
+    content: t('global.GlobalOrder.ConfirmCancelOrder'),
+    success: async (e) => {
+      if (e.confirm) {
+        const res = await ucardApi.getBlockchainWithdrawCancel({ id: detailData.value.id })
+        if (res.code == 200) {
+          // 刷新订单详情
+          uni.showToast({
+            title: t('global.GlobalOrder.CancelOrderSuccess'),
+            icon: 'success'
+          });
+        }
+      }
+    }
+  });
+};
+
+const isLoading = ref(false) // 接口加载状态
+
+const detailData = ref<any>({})
+const list = ref<Record<string, any[]>>({})
+
+const complianceItems = ref<any[]>([])
+const globalForm = ref<any>({})
+
+const complianceStatus = ref(false)
+
+// 页面加载时,先使用缓存数据渲染,然后获取最新数据
+onLoad((e) => {
+  const cachedData = cardStore.orderDetail
+  if (cachedData) {
+    try {
+      const clonedData = JSON.parse(JSON.stringify(cachedData))
+      Object.assign(detailData.value, clonedData)
+    } catch (error) {
+      console.error('加载缓存数据失败:', error)
+    }
+  }
+});
+
+// 离开页面时清空订单详情
+onUnload(() => {
+  cardStore.clearOrderDetail();
+});
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.page-wrapper {
+  padding: 0;
+}
+
+.success-icon-wrap {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-wrap: wrap;
+  background-color: #ffffff;
+
+  image {
+    width: 100%;
+  }
+
+  .success-text {
+    width: 100%;
+    text-align: center;
+    font-size: px2rpx(18);
+    color: #111827;
+    margin-bottom: px2rpx(24);
+  }
+}
+
+.order-detail-page {
+  background-color: #f9fafb;
+  padding-bottom: px2rpx(80);
+}
+
+/* Header */
+.header {
+  background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);
+  padding: px2rpx(12) px2rpx(16);
+  padding-top: calc(px2rpx(12) + env(safe-area-inset-top));
+}
+
+.header-nav {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.back-btn {
+  width: px2rpx(40);
+  height: px2rpx(40);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.header-title {
+  color: #ffffff;
+  font-size: px2rpx(18);
+}
+
+.header-action {
+  width: px2rpx(40);
+}
+
+/* Content */
+.content {
+  padding: px2rpx(16);
+}
+
+/* Status Card */
+.status-card {
+  background-color: #ffffff;
+  border-radius: px2rpx(16);
+  padding: px2rpx(32) px2rpx(24);
+  margin-bottom: px2rpx(16);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.status-icon-wrapper {
+  margin-bottom: px2rpx(16);
+}
+
+.status-icon {
+  width: px2rpx(80);
+  height: px2rpx(80);
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.status-icon-success {
+  background-color: #f0fdf4;
+}
+
+.status-icon-processing {
+  background-color: #fefce8;
+}
+
+.status-icon-failed {
+  background-color: #fef2f2;
+}
+
+.status-icon-cancelled {
+  background-color: #f9fafb;
+}
+
+.status-title {
+  font-size: px2rpx(22);
+  color: #111827;
+  margin-bottom: px2rpx(8);
+}
+
+.status-subtitle {
+  font-size: px2rpx(14);
+  color: #6b7280;
+  text-align: center;
+}
+
+/* Section Card */
+.section-card {
+  background-color: #ffffff;
+  border-radius: px2rpx(12);
+  padding: px2rpx(16);
+  margin-bottom: px2rpx(16);
+}
+
+.section-header {
+  display: flex;
+  align-items: center;
+  gap: px2rpx(8);
+  margin-bottom: px2rpx(16);
+}
+
+.section-title {
+  font-size: px2rpx(16);
+  color: #111827;
+}
+
+/* Approval Timeline */
+.approval-timeline {
+  display: flex;
+  flex-direction: column;
+}
+
+.timeline-item {
+  display: flex;
+  gap: px2rpx(12);
+}
+
+.timeline-left {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  flex-shrink: 0;
+}
+
+.timeline-dot {
+  width: px2rpx(24);
+  height: px2rpx(24);
+  border-radius: 50%;
+  background-color: #f3f4f6;
+  border: 2px solid #e5e7eb;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+
+.timeline-dot-active {
+  background-color: #22c55e;
+  border-color: #22c55e;
+}
+
+.timeline-dot-current {
+  background-color: #eab308;
+  border-color: #eab308;
+  animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+
+  0%,
+  100% {
+    opacity: 1;
+  }
+
+  50% {
+    opacity: 0.7;
+  }
+}
+
+.timeline-line {
+  width: px2rpx(2);
+  flex: 1;
+  background-color: #e5e7eb;
+  margin: px2rpx(4) 0;
+}
+
+.timeline-right {
+  flex: 1;
+  padding-bottom: px2rpx(24);
+}
+
+.timeline-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: px2rpx(6);
+}
+
+.timeline-title {
+  font-size: px2rpx(15);
+  color: #111827;
+}
+
+.timeline-status {
+  display: flex;
+  align-items: center;
+  gap: px2rpx(4);
+  padding: px2rpx(2) px2rpx(8);
+  border-radius: px2rpx(12);
+}
+
+.timeline-status.completed {
+  background-color: #f0fdf4;
+}
+
+.timeline-status.current {
+  background-color: #fefce8;
+}
+
+.timeline-status.pending {
+  background-color: #f9fafb;
+}
+
+.timeline-status-text {
+  font-size: px2rpx(12);
+}
+
+.timeline-status.completed .timeline-status-text {
+  color: #22c55e;
+}
+
+.timeline-status.current .timeline-status-text {
+  color: #eab308;
+}
+
+.timeline-status.pending .timeline-status-text {
+  color: #9ca3af;
+}
+
+.timeline-operator {
+  font-size: px2rpx(13);
+  color: #6b7280;
+  display: block;
+  margin-bottom: px2rpx(4);
+}
+
+.timeline-time {
+  font-size: px2rpx(12);
+  color: #9ca3af;
+  display: block;
+  margin-bottom: px2rpx(4);
+}
+
+.timeline-remark {
+  font-size: px2rpx(13);
+  color: #6b7280;
+  display: block;
+  margin-top: px2rpx(6);
+  padding: px2rpx(8);
+  background-color: #f9fafb;
+  border-radius: px2rpx(6);
+}
+
+/* Info List */
+.info-list {
+  display: flex;
+  flex-direction: column;
+  gap: px2rpx(12);
+}
+
+.info-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: px2rpx(12);
+  padding: px2rpx(4) 0;
+}
+
+.info-row.vertical {
+  flex-direction: column;
+  align-items: flex-start;
+}
+
+.info-label {
+  font-size: px2rpx(14);
+  color: #6b7280;
+  flex-shrink: 0;
+}
+
+.info-value {
+  font-size: px2rpx(14);
+  color: #111827;
+  text-align: right;
+  word-break: break-all;
+}
+
+.info-value-wrapper {
+  display: flex;
+  align-items: center;
+  gap: px2rpx(8);
+  flex: 1;
+  justify-content: flex-end;
+}
+
+.amount-highlight {
+  font-size: px2rpx(20);
+  color: #2563eb;
+}
+
+.total-label {
+  font-size: px2rpx(15);
+  color: #111827;
+}
+
+.total-value {
+  font-size: px2rpx(18);
+  color: #ef4444;
+}
+
+.remark-text {
+  text-align: left;
+  color: #6b7280;
+  line-height: 1.6;
+}
+
+.divider {
+  height: px2rpx(1);
+  background-color: #f3f4f6;
+  margin: px2rpx(4) 0;
+}
+
+/* Service Card */
+.service-card {
+  background-color: #ffffff;
+  border-radius: px2rpx(12);
+  padding: px2rpx(16);
+  display: flex;
+  align-items: center;
+  gap: px2rpx(12);
+  margin-bottom: px2rpx(16);
+}
+
+.service-text {
+  flex: 1;
+  font-size: px2rpx(15);
+  color: #111827;
+}
+
+/* Bottom Actions */
+.bottom-actions {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background-color: #ffffff;
+  border-top: 1px solid #e5e7eb;
+  padding: px2rpx(12) px2rpx(16);
+  padding-bottom: calc(px2rpx(12) + env(safe-area-inset-bottom));
+  display: flex;
+  gap: px2rpx(12);
+}
+
+.action-btn {
+  flex: 1;
+  height: px2rpx(44);
+  border-radius: px2rpx(8);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.cancel-btn {
+  background-color: #f3f4f6;
+}
+
+.cancel-btn .btn-text {
+  color: #6b7280;
+}
+
+.appeal-btn {
+  background-color: #2563eb;
+}
+
+.appeal-btn .btn-text {
+  color: #ffffff;
+}
+
+.delete-btn {
+  background-color: #f3f4f6;
+}
+
+.delete-btn .btn-text {
+  color: #ef4444;
+}
+
+.btn-text {
+  font-size: px2rpx(15);
+}
+
+.currency {
+  font-size: px2rpx(12);
+}
+</style>

+ 277 - 0
pages/wallet/withdraw-list.vue

@@ -0,0 +1,277 @@
+<template>
+    <cwg-page-wrapper>
+        <view class="bank-transaction-page">
+            <view class="filters-container" :style="{ top: statusBarHeight + 53 + 'px' }">
+                <view class="filter-item">
+                    <text class="filter-label">{{ t('card.Form.f45') }}</text>
+                    <cwg-picker v-model="statusFilterIndex" :options="statusOptions" />
+                </view>
+                <view class="filter-item">
+                    <text class="filter-label">{{ t('card.Form.f43') }}</text>
+                    <view class="filter-picker" @click="open">
+                        <view class="picker-value">
+                            <text class="picker-text">{{ dateFilter || t('card.Form.f57') }}</text>
+                            <uni-icons type="calendar" size="14" color="#6b7280" class="picker-icon" />
+                        </view>
+                    </view>
+                    <cwg-date-picker v-model:show="show" v-model="dateFilter" mode="date" @confirm="onDateConfirm"
+                        :minDate="minDate" :maxDate="maxDate" />
+                </view>
+                <view class="reset-btn" @click="resetFilters">
+                    <uni-icons type="loop" size="16" color="#2563eb" />
+                </view>
+            </view>
+            <view class="content">
+                <WithdrawList ref="globalListRef" :cardNo="cardNo" :payoutCurrency="payoutCurrency"
+                    :statusIndex="statusFilterIndex" :dateFilter="dateFilter" />
+            </view>
+        </view>
+    </cwg-page-wrapper>
+
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch } from 'vue';
+import { onLoad } from '@dcloudio/uni-app';
+import { useI18n } from 'vue-i18n';
+import useGlobalStore from '@/stores/use-global-store';
+import WithdrawList from './components/WithdrawList.vue';
+import { withdrawStatus } from '@/utils/dataMap';
+import useCardStore from "@/stores/use-card-store";
+const cardStore = useCardStore();
+const globalStore = useGlobalStore()
+const statusBarHeight = computed(() => globalStore.statusBarHeight);
+const { t } = useI18n();
+const minDate = ref(new Date(2000, 0, 1).getTime());
+const maxDate = ref(new Date().getTime());
+const cardNo = ref('');
+onLoad((options) => {
+    cardNo.value = options.cardNo || '';
+});
+
+const statusOptions = computed(() => {
+    return withdrawStatus
+});
+const payoutCurrency = ref();
+const statusFilterIndex = ref(0);
+const dateFilter = ref('');
+
+const show = ref(false)
+const onDateConfirm = (e: any) => {
+    dateFilter.value = e.formatted;
+};
+function open() {
+    show.value = true
+}
+
+const resetFilters = () => {
+    payoutCurrency.value = '';
+    statusFilterIndex.value = 0;
+    dateFilter.value = '';
+};
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.page-wrapper {
+    padding: 0;
+    border: 0;
+}
+
+.bank-transaction-page {
+    // background-color: #f9fafb;
+}
+
+.wallet-header {
+    background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);
+}
+
+/* Header */
+.header {
+    background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);
+    padding: px2rpx(16);
+    padding-bottom: px2rpx(20);
+}
+
+.header-content {
+    display: flex;
+    flex-direction: column;
+    gap: px2rpx(16);
+}
+
+.header-title {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(8);
+}
+
+.title-text {
+    color: #ffffff;
+    font-size: px2rpx(20);
+}
+
+.stats-container {
+    display: flex;
+    gap: px2rpx(12);
+}
+
+.stat-card {
+    flex: 1;
+    background-color: rgba(255, 255, 255, 0.15);
+    backdrop-filter: blur(px2rpx(10));
+    border-radius: px2rpx(12);
+    padding: px2rpx(12);
+}
+
+.stat-header {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(6);
+    margin-bottom: px2rpx(8);
+}
+
+.icons {
+    width: px2rpx(20);
+    height: px2rpx(20);
+}
+
+.stat-label {
+    font-size: px2rpx(12);
+    color: rgba(255, 255, 255, 0.9);
+}
+
+.stat-value {
+    font-size: px2rpx(20);
+    color: #ffffff;
+    display: block;
+    margin-bottom: px2rpx(4);
+}
+
+.stat-count {
+    font-size: px2rpx(11);
+    color: rgba(255, 255, 255, 0.8);
+}
+
+/* Tabs */
+.tabs-container {
+    background-color: #ffffff;
+    display: flex;
+    border-bottom: 1px solid #e5e7eb;
+    position: sticky;
+    top: 0;
+    z-index: 10;
+}
+
+.tab-item {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: px2rpx(6);
+    padding: px2rpx(16);
+    position: relative;
+    transition: all 0.3s;
+}
+
+.tab-text {
+    font-size: px2rpx(15);
+    color: #9ca3af;
+    transition: color 0.3s;
+}
+
+.tab-text-active {
+    color: #2563eb;
+}
+
+.tab-indicator {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    height: px2rpx(3);
+    background-color: #2563eb;
+    border-radius: px2rpx(3) px2rpx(3) 0 0;
+}
+
+/* Content */
+.content {
+    padding: 0;
+}
+
+.filters-container {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(8);
+    padding: px2rpx(12) px2rpx(16);
+    background-color: #ffffff;
+    position: sticky;
+    top: 0;
+    z-index: 10;
+    overflow-x: auto;
+    -webkit-overflow-scrolling: touch;
+}
+
+.filter-item {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(4);
+    flex-shrink: 0;
+    min-width: 0;
+}
+
+.filter-label {
+    font-size: px2rpx(12);
+    color: #6b7280;
+    white-space: nowrap;
+    flex-shrink: 0;
+}
+
+.filter-picker {
+    background-color: #ffffff;
+    border: 1px solid #e5e7eb;
+    border-radius: px2rpx(8);
+    padding: px2rpx(6) px2rpx(8);
+    display: flex;
+    align-items: center;
+    min-width: px2rpx(60);
+    max-width: px2rpx(120);
+    flex-shrink: 1;
+}
+
+.picker-value {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(4);
+    width: 100%;
+    min-width: 0;
+}
+
+.picker-text {
+    flex: 1;
+    min-width: 0;
+    font-size: px2rpx(12);
+    color: #111827;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+.picker-icon {
+    flex-shrink: 0;
+    width: px2rpx(14);
+    height: px2rpx(14);
+}
+
+.reset-btn {
+    background-color: #ffffff;
+    border: 1px solid #e5e7eb;
+    border-radius: px2rpx(8);
+    padding: px2rpx(6) px2rpx(10);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+    min-width: px2rpx(36);
+}
+</style>

+ 339 - 0
pages/wallet/withdraw.vue

@@ -0,0 +1,339 @@
+<template>
+    <cwg-page-wrapper>
+        <view class="page">
+            <u-form ref="formRef" :rules="rules" :model="form" class="payment-form">
+                <cwg-input v-model:value="form.blockchain" fkey="blockchain" type="select" :required="true"
+                    :columns="blockchainList" :label="t('Blockchain.addP2')" rulesKey="blockchain"
+                    @change="handleChange" />
+                <cwg-input :label="t('WalletApply.p1')" v-model:value="form.address" fkey="address" type="text"
+                    rulesKey="address" :placeholder="t('WalletApply.p2')" :required="true" clearable />
+                <cwg-input :label="`${t('WalletApply.p3')} ${t('WalletApply.p5', { userBalance: form.userBalance })}`"
+                    rulesKey="amount" v-model:value="form.amount" fkey="amount" type="number"
+                    :placeholder="t('WalletApply.p4')" :required="true" @change="globalExchangeRate" clearable />
+
+                <view>
+                    <view class="code-input-wrapper">
+                        <view class="code-input">
+                            <cwg-input v-model:value="form.emailCode" :label="t('newSignup.item9')" fkey="emailCode"
+                                type="text" :required="true" rulesKey="emailCode" :placeholder="t('newSignup.item10')"
+                                @change="handleChange" />
+                        </view>
+                        <view class="get-code-btn1">
+                            <view class="ok-button">
+                                <u-button type="primary" block>{{ getCodeString }}</u-button>
+                            </view>
+                        </view>
+                        <view class="get-code-btn">
+                            <view class="cwg-button">
+                                <u-button type="primary" block @click="handleGetCode">{{ getCodeString }}</u-button>
+                            </view>
+                        </view>
+                    </view>
+                </view>
+            </u-form>
+
+            <view class="fixed-btn">
+                <view class="cwg-button">
+                    <u-button type="primary" block @click="infoSubmit">{{ t("card.Btn.Submit") }}</u-button>
+                </view>
+            </view>
+        </view>
+        <cwg-SuccessPrompt v-model:show="showSuccessPrompt" :title="t('pages.wallet.withdraw')"
+            :desc="t('Custom.Withdraw.Des1')" :btn-click="btnClick" />
+    </cwg-page-wrapper>
+</template>
+<script setup lang="ts">
+import { ref, onMounted, watch, computed, onBeforeUnmount } from "vue";
+import { showToast } from "@/utils/toast";
+import { useI18n } from "vue-i18n";
+import { ucardApi } from "@/api/ucard";
+import useUserStore from "@/stores/use-user-store";
+import useRouter from "@/hooks/useRouter";
+import { Validators } from "@/utils/validators";
+import { onLoad } from '@dcloudio/uni-app'
+onLoad((options) => {
+    userBalance.value = options.allAmount
+})
+const userBalance = ref(0);
+const router = useRouter();
+const { t } = useI18n();
+const userStore = useUserStore();
+const userInfo = computed(() => userStore.userInfo);
+const formRef = ref();
+const form = ref<{
+    emailCode: string;
+    cardNo?: string;
+    password?: string;
+    country?: string;
+    email?: string;
+}>({
+    emailCode: "",
+});
+const text1 = ref("");
+const blockchainList = ref([]);
+const timer = ref(59);
+const getCodeString = ref("");
+const interval = ref<NodeJS.Timeout | null>(null);
+// 表单验证规则
+const rules = {
+    blockchain: [Validators.required(t("Blockchain.addP2"))],
+    address: [Validators.required(t("WalletApply.p2"))],
+    amount: [
+        Validators.required(t("global.validator.v15")),
+        Validators.custom(validateAmount),
+    ],
+};
+function validateAmount(a: any, b?: any, c?: any) {
+    if (typeof c === "function") {
+        const value = b;
+        const callback = c;
+        const val = String(value ?? "").trim();
+        const num = Number(val);
+        const pattern = /^(?:[1-9]\d*(?:\.\d{1,2})?|0\.(?!0+$)\d{1,2})$/;
+        if (!pattern.test(val)) {
+            callback(new Error(t("global.validator.v15")));
+            return;
+        }
+        if (isNaN(num) || num <= 0) {
+            callback(new Error(t("global.validator.v15")));
+            return;
+        }
+        const balance = Number(form.value.userBalance);
+        if (!isNaN(balance) && num > balance) {
+            const msg = t("WalletApply.p6", { userBalance: form.value.userBalance });
+            callback(new Error(msg));
+            return;
+        }
+        return callback();
+    }
+}
+function btnClick() {
+    showSuccessPrompt.value = false;
+    router.push({ path: "/pages/wallet/index" })
+}
+// 初始化表单
+function initForm() {
+    const a = { uniqueId: userInfo.value.uniqueId, country: userInfo.value.country, email: userInfo.value.email }
+    if (userInfo.value) {
+        form.value = {
+            ...a,
+            userBalance: userBalance.value,
+            emailCode: "",
+        };
+    }
+    text1.value = "";
+    timer.value = 59;
+    getCodeString.value = t("newSignup.item11");
+    if (interval.value) {
+        clearInterval(interval.value);
+        interval.value = null;
+    }
+}
+// 初始化定时器
+function initTimer() {
+    const savedTimer = Number(localStorage.getItem("cvvTimer")) || 59;
+    if (savedTimer === 59) {
+        getCodeString.value = t("newSignup.item11");
+    } else {
+        timer.value = savedTimer;
+        startTimer();
+    }
+}
+
+// 开始倒计时
+function startTimer() {
+    if (interval.value) {
+        return;
+    }
+    getCodeString.value = `${t("signup.form.waitCode1")}${timer.value}${t(
+        "signup.form.waitCode2"
+    )}`;
+    interval.value = setInterval(() => {
+        timer.value--;
+        localStorage.setItem("cvvTimer", timer.value.toString());
+        if (timer.value > 0) {
+            getCodeString.value = `${t("signup.form.waitCode1")}${timer.value}${t(
+                "signup.form.waitCode2"
+            )}`;
+        } else {
+            getCodeString.value = t("newSignup.item11");
+            if (interval.value) {
+                clearInterval(interval.value);
+                interval.value = null;
+            }
+            timer.value = 59;
+            localStorage.setItem("cvvTimer", "59");
+        }
+    }, 1000);
+}
+const showSuccessPrompt = ref(false);
+// 提交表单
+async function infoSubmit() {
+    try {
+        await formRef.value?.validate();
+        let res = await ucardApi.getBlockchainWithdrawApply({
+            ...form.value,
+        });
+        if (res.code == 200) {
+            showSuccessPrompt.value = true;
+        } else {
+            showToast(res.msg);
+        }
+    } catch (error) {
+        console.log(error, 1111);
+
+    }
+}
+//获取区块链
+async function getBlockchainDropdown() {
+    let res = await ucardApi.getBlockchainDropdown({
+        ...form.value,
+        type: '2'
+    });
+    if (res.code == 200) {
+        res.data.map((item: any) => {
+            item.text = item.alias;
+            item.value = item.blockchain;
+        });
+        blockchainList.value = res.data;
+    } else {
+        blockchainList.value = []
+        showToast(res.msg);
+    }
+}
+
+// 发送邮箱验证码
+async function sendEmailCode() {
+    try {
+        if (!form.value.country) {
+            showToast(t("vaildate.country.empty"));
+            return false;
+        }
+        if (!form.value.email) {
+            showToast(t("vaildate.email.empty"));
+            return false;
+        }
+
+        const res = await ucardApi.getBlockchainWithdrawSendEmailCode({
+            ...form.value,
+        });
+
+        if (res.code === 200) {
+            showToast(t("Msg.CodeSuccess"));
+            startTimer();
+            return true;
+        } else {
+            showToast(t("Msg.CodeFail"));
+            return false;
+        }
+    } catch (error: any) {
+        console.log(error, 12);
+
+        showToast(t("Msg.CodeFail"));
+        return false;
+    }
+}
+
+// 获取验证码按钮点击
+async function handleGetCode() {
+    if (timer.value > 0 && timer.value < 59) {
+        return;
+    }
+    text1.value = "";
+    await sendEmailCode();
+}
+
+// 表单字段变化
+function handleChange(value: any) {
+    if (value.key === "emailCode") {
+        form.value.emailCode = value.value;
+    }
+}
+
+
+onMounted(() => {
+    initForm();
+    initTimer();
+    getBlockchainDropdown();
+});
+
+// 组件卸载时清理
+onBeforeUnmount(() => {
+    if (interval.value) {
+        clearInterval(interval.value);
+        interval.value = null;
+    }
+});
+</script>
+
+<style lang="scss" scoped>
+@import "@/uni.scss";
+
+.no-button {
+    width: 100%;
+    margin: px2rpx(12) 0;
+
+    .u-button {
+        background-color: #ffbdc8 !important;
+    }
+}
+
+
+.code-input-label {
+    font-size: var(--font-size-16);
+    line-height: px2rpx(44);
+    letter-spacing: px2rpx(1);
+    color: #474747;
+}
+
+.code-input-wrapper {
+    position: relative;
+    display: flex;
+    align-items: center;
+}
+
+.code-input {
+    flex: 1;
+}
+
+.get-code-btn1 {
+    min-width: px2rpx(100);
+    margin-left: px2rpx(8);
+
+    .ok-button {
+
+        .u-button {
+            background-color: #fff;
+            opacity: 0;
+        }
+    }
+
+}
+
+.get-code-btn {
+    position: absolute;
+    right: 0;
+    bottom: px2rpx(12);
+    min-width: px2rpx(100);
+    background-color: #fff;
+    z-index: 1;
+    margin-left: px2rpx(8);
+
+    .cwg-button {
+        margin: 0;
+    }
+
+    .cwg-button .u-button {
+        border-radius: px2rpx(8);
+        height: px2rpx(46) !important;
+    }
+}
+
+.submit-section {
+    margin: px2rpx(20) 0;
+}
+
+.submit-btn {
+    width: 100%;
+}
+</style>

BIN
unpackage/res/icons/1024x1024.png


BIN
unpackage/res/icons/120x120.png


BIN
unpackage/res/icons/144x144.png


BIN
unpackage/res/icons/152x152.png


BIN
unpackage/res/icons/167x167.png


BIN
unpackage/res/icons/180x180.png


BIN
unpackage/res/icons/192x192.png


BIN
unpackage/res/icons/20x20.png


BIN
unpackage/res/icons/29x29.png


BIN
unpackage/res/icons/40x40.png


BIN
unpackage/res/icons/58x58.png


BIN
unpackage/res/icons/60x60.png


BIN
unpackage/res/icons/72x72.png


BIN
unpackage/res/icons/76x76.png


BIN
unpackage/res/icons/80x80.png


BIN
unpackage/res/icons/87x87.png


BIN
unpackage/res/icons/96x96.png


+ 20 - 0
utils/dataMap.js

@@ -84,5 +84,25 @@ export const rechargeType = {
   "2": "card.Form.f62",
   "3": "card.Form.f63"
 }
+// 加密货币充值状态
+export const vaultodyStatusText = {
+  1: "global.GlobalOrder.success",
+  2: "global.GlobalOrder.fail",
+};
+// 加密货币提现状态
+export const withdrawStatus = {
+  1: "card.Status.t5",
+  2: "card.Status.t3",
+  3: "card.Status.t1",
+  4: "card.Status.t2",
+  5: "card.Btn.Cancel",
+};
+// 加密货币充值审批状态
+export const withdrawApprovalText = {
+  1: "global.Status.Pending",
+  2: "global.Status.Approved",
+  3: "global.Status.Rejected",
+};
+
 
 

+ 3 - 3
utils/request.js

@@ -1,10 +1,10 @@
 // 基础配置
 import { removeUserInfo, removeToken } from "./auth.js";
 import config from "@/config";
-const { Host85, Host00 } = config;
+const { Host85, Host00 } = config;
 // const baseUrl = "https://ucard.cwgrd.com";
 // const baseUrl = "https://ucard.44a5c8109e4.com";
-// const baseUrl = "http://192.168.0.18:8700";
+// const baseUrl = "http://192.168.0.18:8700";
 const baseUrl = Host85;
 const timeout = 10000;
 import { CLIENT, lang, userToken } from "@/composables/config";
@@ -94,7 +94,7 @@ const responseInterceptor = (response, options = {}) => {
       return data;
     } else {
       uni.showToast({
-        title: data.message || "请求失败",
+        title: data.msg || "请求失败",
         icon: "none",
       });
       return Promise.reject(data);