Przeglądaj źródła

新增表格、分页、下拉框等组件,个人信息、银行信息、文件管理、安全中心、通知、支付历史页面

zhb 3 miesięcy temu
rodzic
commit
eb02975c7b
100 zmienionych plików z 9967 dodań i 739 usunięć
  1. 135 0
      components/cwg-combox.vue
  2. 817 0
      components/cwg-file-picker.vue
  3. 169 0
      components/cwg-file.vue
  4. 1 1
      components/cwg-header.vue
  5. 522 0
      components/cwg-input1.vue
  6. 106 66
      components/cwg-page-wrapper.vue
  7. 15 4
      components/cwg-pc-header.vue
  8. 171 0
      components/cwg-right-drawer.vue
  9. 9 8
      components/cwg-sidebar.vue
  10. 662 0
      components/cwg-tabel.vue
  11. 88 0
      components/cwg-video-player.vue
  12. 0 0
      composables/statusMachine.ts
  13. 1 0
      index.html
  14. 40 2
      locale/cn.json
  15. 1 1
      main.js
  16. 2 0
      package.json
  17. 13 99
      pages.json
  18. 0 0
      pages/customer/activities.vue
  19. 85 0
      pages/customer/components/OrderStatusMachineCell.vue
  20. 260 0
      pages/customer/composables/statusMachine.ts
  21. 395 0
      pages/customer/create-account.vue
  22. 0 0
      pages/customer/index.vue
  23. 200 0
      pages/customer/payment-history.vue
  24. 0 0
      pages/customer/recording-history.vue
  25. 1 1
      pages/launch/index.vue
  26. 363 0
      pages/mine/components/AddBankDialog.vue
  27. 261 0
      pages/mine/components/AddFileDialog.vue
  28. 1049 0
      pages/mine/components/BankInfoTab.vue
  29. 583 0
      pages/mine/components/BankItem.vue
  30. 562 0
      pages/mine/components/CardAuthDialog.vue
  31. 234 0
      pages/mine/components/FileManagementTab.vue
  32. 0 62
      pages/mine/components/InfoRow.vue
  33. 377 0
      pages/mine/components/KycAuthDialog.vue
  34. 580 0
      pages/mine/components/PersonalInfoTab.vue
  35. 275 0
      pages/mine/components/SecurityCenterTab.vue
  36. 0 276
      pages/mine/components/VerificationRow.vue
  37. 64 0
      pages/mine/components/bank.ts
  38. 348 0
      pages/mine/info copy 2.vue
  39. 238 0
      pages/mine/info copy.vue
  40. 342 180
      pages/mine/info.vue
  41. 444 0
      pages/mine/new.vue
  42. 150 0
      pages/mine/notice.vue
  43. 1 0
      static/icons/crm-angle-left.svg
  44. 1 0
      static/icons/crm-bars-staggered.svg
  45. 1 0
      static/icons/crm-building-columns.svg
  46. 1 0
      static/icons/crm-chart-area.svg
  47. 1 0
      static/icons/crm-circle-dollar-to-slot.svg
  48. 1 0
      static/icons/crm-circle-user.svg
  49. 1 0
      static/icons/crm-clock-rotate-left.svg
  50. 1 0
      static/icons/crm-credit-card.svg
  51. 1 0
      static/icons/crm-diagram.svg
  52. 1 0
      static/icons/crm-download.svg
  53. 1 0
      static/icons/crm-file.svg
  54. 1 0
      static/icons/crm-headset.svg
  55. 1 0
      static/icons/crm-house.svg
  56. 1 0
      static/icons/crm-image.svg
  57. 1 0
      static/icons/crm-leaf.svg
  58. 1 0
      static/icons/crm-lock.svg
  59. 1 0
      static/icons/crm-menu.svg
  60. 1 0
      static/icons/crm-photo-film.svg
  61. 1 0
      static/icons/crm-plus.svg
  62. 1 0
      static/icons/crm-star.svg
  63. 1 0
      static/icons/crm-trash-can.svg
  64. 1 0
      static/icons/crm-user-pen.svg
  65. 1 0
      static/icons/crm-user.svg
  66. 1 0
      static/icons/crm-users.svg
  67. 1 0
      static/icons/crm-wallet.svg
  68. BIN
      static/images/img/Notifications.png
  69. BIN
      static/images/img/acc_logo.png
  70. BIN
      static/images/img/account-reg-1.png
  71. BIN
      static/images/img/account-reg-2.png
  72. BIN
      static/images/img/account-reg-3.png
  73. BIN
      static/images/img/applicaiton-history.png
  74. BIN
      static/images/img/bank-info.png
  75. BIN
      static/images/img/dashboard.png
  76. BIN
      static/images/img/file-management.png
  77. BIN
      static/images/img/login.png
  78. BIN
      static/images/img/payment-history.png
  79. BIN
      static/images/img/personal-info.png
  80. BIN
      static/images/img/promotions.png
  81. BIN
      static/images/img/registration.png
  82. BIN
      static/images/img/security-center.png
  83. BIN
      static/images/img/sidebar-menu.png
  84. BIN
      static/images/info/bank-information-1.webp
  85. BIN
      static/images/info/bank-information-2.webp
  86. BIN
      static/images/info/bank-information-3.webp
  87. BIN
      static/images/info/success.png
  88. BIN
      static/images/trading/android.png
  89. BIN
      static/images/trading/apple.png
  90. BIN
      static/images/trading/icone-windows-ios.png
  91. BIN
      static/images/trading/icone-windows.png
  92. BIN
      static/images/trading/ios.png
  93. BIN
      static/images/trading/trading-instruments-1.png
  94. BIN
      static/images/trading/trading-instruments-2.png
  95. BIN
      static/images/trading/trading-instruments-3.png
  96. BIN
      static/images/trading/trading-instruments-4.png
  97. BIN
      static/images/trading/trading-instruments-6.png
  98. BIN
      static/images/trading/trading-instruments-7.png
  99. 0 0
      static/js/jsvm_all.js
  100. 379 39
      static/scss/global/global.scss

+ 135 - 0
components/cwg-combox.vue

@@ -0,0 +1,135 @@
+<template>
+    <view class="cwg-combox">
+
+        <template v-if="disabled">
+            <view class="disabled-text">{{ innerText }}</view>
+
+        </template>
+        <template v-if="!disabled">
+            <!-- 可搜索模式 -->
+            <uni-combox v-if="filterable" v-model="innerText" :candidates="textList" :clearable="clearable"
+                :placeholder="placeholder" :disabled="disabled" @input="handleComboxChange" />
+
+            <!-- 普通下拉模式 -->
+            <uni-data-select v-else v-model="innerValue" :localdata="options" :clear="clearable"
+                :placeholder="placeholder" :disabled="disabled" @change="handleSelectChange" />
+        </template>
+
+    </view>
+</template>
+
+<script setup>
+import { ref, computed, watch, onMounted } from 'vue'
+
+const props = defineProps({
+    value: [String, Number],
+    options: {
+        type: Array,
+        default: () => []
+    },
+    filterable: {
+        type: Boolean,
+        default: false
+    },
+    placeholder: String,
+    clearable: {
+        type: Boolean,
+        default: true
+    },
+    disabled: Boolean
+})
+
+const emit = defineEmits(['update:value', 'change'])
+
+/**
+ * 内部状态
+ */
+const innerValue = ref(props.value)
+const innerText = ref('')
+
+/**
+ * 提取 text 列表(给 combox 用)
+ */
+const textList = computed(() =>
+    props.options.map(item => item.text)
+)
+
+/**
+ * value → text 映射
+ */
+const updateTextByValue = (val) => {
+    if (!val) {
+        innerText.value = ''
+        return
+    }
+    const item = props.options.find(i => i.value === val)
+    innerText.value = item ? item.text : ''
+}
+
+/**
+ * 组件挂载时初始化
+ */
+onMounted(() => {
+    if (props.value) {
+        innerValue.value = props.value
+        updateTextByValue(props.value)
+    }
+})
+
+/**
+ * value 改变时同步内部状态
+ */
+watch(
+    () => props.value,
+    (val) => {
+        innerValue.value = val
+        updateTextByValue(val)
+    }
+)
+
+/**
+ * options 改变时重新计算显示文本(回显)
+ */
+watch(
+    () => props.options,
+    () => {
+        if (props.value) {
+            updateTextByValue(props.value)
+        }
+    },
+    { deep: true }
+)
+
+/**
+ * 普通 select 改变
+ */
+const handleSelectChange = (val) => {
+    innerValue.value = val
+    updateTextByValue(val)
+    emit('update:value', val)
+    emit('change', val)
+}
+
+/**
+ * combox 改变
+ */
+const handleComboxChange = (text) => {
+    const item = props.options.find(i => i.text === text)
+    if (item) {
+        innerValue.value = item.value
+        emit('update:value', item.value)
+        emit('change', item.value)
+    }
+}
+</script>
+<style lang="scss" scoped>
+.disabled-text {
+    height: px2rpx(35);
+    padding: 0 px2rpx(12);
+    border-radius: px2rpx(4);
+    line-height: px2rpx(35);
+    font-size: px2rpx(15);
+    background: #f5f5f5;
+    color: #d5d5d5;
+}
+</style>

+ 817 - 0
components/cwg-file-picker.vue

@@ -0,0 +1,817 @@
+<template>
+    <view class="common-file-uploader" :class="customClass">
+        <!-- 编辑状态:显示可上传的文件选择器 -->
+        <view v-if="editable" class="upload-wrapper">
+            <uni-file-picker :limit="multiple ? limit : 1" :title="title" :file-mediatype="fileMediatype" :mode="mode"
+                :auto-upload="false" :value="fileList" :disabled="disabled" :readonly="readonly"
+                :image-styles="imageStyles" :list-styles="listStyles" @select="handleSelect" @delete="handleDelete">
+                <!-- 自定义上传按钮(单张图片且已有图片时显示替换) -->
+                <template v-if="$slots.default || showCustomButton">
+                    <slot name="default">
+                        <view class="custom-upload-btn" :class="{ 'replace-btn': !multiple && fileList.length > 0 }"
+                            :style="customButtonStyle">
+                            <template v-if="!multiple && fileList.length > 0">
+                                <text class="replace-icon">↻</text>
+                                <text class="tip">{{ replaceText || t('Common.Replace') }}</text>
+                            </template>
+                            <template v-else>
+                                <text class="plus">+</text>
+                                <text class="tip">{{ uploadText || t('Common.Upload') }}</text>
+                            </template>
+                        </view>
+                    </slot>
+                </template>
+            </uni-file-picker>
+
+            <!-- 上传进度 -->
+            <view v-if="showProgress && uploadProgress > 0 && uploadProgress < 100" class="upload-progress"
+                :style="progressStyle">
+                <view class="progress-bar" :style="{ width: uploadProgress + '%' }"></view>
+                <text class="progress-text">{{ uploadProgress }}%</text>
+            </view>
+
+            <!-- 上传错误提示 -->
+            <view v-if="uploadError" class="upload-error">
+                <text class="error-text">{{ uploadError }}</text>
+            </view>
+        </view>
+
+        <!-- 非编辑状态:显示图片预览 -->
+        <view v-else class="image-preview" :class="previewClass">
+            <!-- 单张图片预览 -->
+            <template v-if="!multiple">
+                <view class="single-preview">
+                    <image v-if="getImageUrl(modelValue)" :src="getImageUrl(modelValue)" :mode="imageMode"
+                        class="preview-image" :style="previewImageStyle" @tap="handlePreview(getImageUrl(modelValue))"
+                        @error="handleImageError" :lazy-load="true" />
+                    <view v-else class="no-image" :style="noImageStyle">
+                        <text :style="noImageTextStyle">{{ noImageText || t('Common.NoImage') }}</text>
+                    </view>
+                </view>
+            </template>
+
+            <!-- 多张图片预览 -->
+            <template v-else>
+                <view v-if="getFileList(modelValue).length" class="image-list" :style="listContainerStyle">
+                    <view v-for="(file, idx) in getFileList(modelValue)" :key="idx" class="image-item"
+                        :style="imageItemStyle">
+                        <image :src="getFileUrl(file)" :mode="imageMode" class="preview-image"
+                            :style="previewImageStyle" @tap="handlePreview(getFilePath(file))" @error="handleImageError"
+                            :lazy-load="true" />
+
+                        <!-- 预览时的删除按钮(如果有权限) -->
+                        <view v-if="showPreviewDelete && canDelete" class="preview-delete-btn" :style="deleteBtnStyle"
+                            @tap.stop="handlePreviewDelete(file, idx)">
+                            <text class="delete-icon">×</text>
+                        </view>
+                    </view>
+                </view>
+                <view v-else class="no-image" :style="noImageStyle">
+                    <text :style="noImageTextStyle">{{ noImageText || t('Common.NoImage') }}</text>
+                </view>
+            </template>
+        </view>
+    </view>
+</template>
+
+<script setup>
+import { computed, ref, watch } from 'vue'
+import { useI18n } from 'vue-i18n'
+import config from '@/config';
+import { userToken } from '@/composables/config'
+const { t } = useI18n()
+
+const props = defineProps({
+    // 基础配置
+    modelValue: {
+        type: [String, Array, Object],
+        default: null
+    },
+    editable: {
+        type: Boolean,
+        default: false
+    },
+    multiple: {
+        type: Boolean,
+        default: false
+    },
+    limit: {
+        type: Number,
+        default: 1
+    },
+
+    // 上传配置
+    title: {
+        type: String,
+        default: ''
+    },
+    fileMediatype: {
+        type: String,
+        default: 'image'
+    },
+    mode: {
+        type: String,
+        default: 'grid'
+    },
+    disabled: {
+        type: Boolean,
+        default: false
+    },
+    readonly: {
+        type: Boolean,
+        default: false
+    },
+
+    // 上传API配置
+    uploadUrl: {
+        type: String,
+        required: true,
+        default: '/custom/bank/upload'
+    },
+    uploadHeaders: {
+        type: Object,
+        default: () => ({})
+    },
+    uploadName: {
+        type: String,
+        default: 'file'
+    },
+    uploadData: {
+        type: Object,
+        default: () => ({})
+    },
+
+    // 图片服务配置
+    baseUrl: {
+        type: String,
+        default: config.Host80
+    },
+    imagePathPrefix: {
+        type: String,
+        default: ''
+    },
+
+    // 样式配置
+    customClass: {
+        type: String,
+        default: ''
+    },
+    previewClass: {
+        type: String,
+        default: ''
+    },
+
+    // 尺寸配置
+    imageWidth: {
+        type: [String, Number],
+        default: 160
+    },
+    imageHeight: {
+        type: [String, Number],
+        default: 160
+    },
+    imageGap: {
+        type: [String, Number],
+        default: 16
+    },
+    imageBorderRadius: {
+        type: [String, Number],
+        default: 8
+    },
+
+    // 图片配置
+    imageMode: {
+        type: String,
+        default: 'aspectFill'
+    },
+
+    // 上传按钮配置
+    showCustomButton: {
+        type: Boolean,
+        default: true
+    },
+    uploadText: {
+        type: String,
+        default: ''
+    },
+    replaceText: {
+        type: String,
+        default: ''
+    },
+
+    // 暂无图片配置
+    noImageText: {
+        type: String,
+        default: ''
+    },
+
+    // 预览配置
+    showPreviewDelete: {
+        type: Boolean,
+        default: false
+    },
+    canDelete: {
+        type: Boolean,
+        default: true
+    },
+
+    // 进度显示
+    showProgress: {
+        type: Boolean,
+        default: true
+    },
+
+    // 是否自动上传
+    autoUpload: {
+        type: Boolean,
+        default: true
+    },
+
+    // 响应数据处理函数
+    responseHandler: {
+        type: Function,
+        default: (res) => {
+            try {
+                const data = typeof res === 'string' ? JSON.parse(res) : res
+                return {
+                    success: data.code === 200,
+                    path: data.data?.path || data.data,
+                    message: data.msg || '上传成功',
+                    data: data
+                }
+            } catch (e) {
+                return {
+                    success: false,
+                    path: null,
+                    message: '解析响应失败',
+                    data: null
+                }
+            }
+        }
+    },
+
+    // uni-file-picker 的图片样式
+    imageStyles: {
+        type: Object,
+        default: () => ({})
+    },
+
+    // uni-file-picker 的列表样式
+    listStyles: {
+        type: Object,
+        default: () => ({})
+    },
+
+    // 自定义样式对象
+    customStyles: {
+        type: Object,
+        default: () => ({})
+    }
+})
+
+const emit = defineEmits([
+    'update:modelValue',
+    'select',
+    'delete',
+    'progress',
+    'success',
+    'fail',
+    'preview',
+    'image-error',
+    'preview-delete',
+    'upload-start',
+    'upload-complete'
+])
+
+// 上传进度
+const uploadProgress = ref(0)
+const uploadError = ref('')
+const isUploading = ref(false)
+
+// 文件列表(用于uni-file-picker回显)
+const fileList = computed(() => {
+    return getFileValue(props.modelValue)
+})
+
+// 计算样式
+const imageWidthPx = computed(() => {
+    return typeof props.imageWidth === 'number' ? px2rpx(props.imageWidth) : props.imageWidth
+})
+
+const imageHeightPx = computed(() => {
+    return typeof props.imageHeight === 'number' ? px2rpx(props.imageHeight) : props.imageHeight
+})
+
+const imageGapPx = computed(() => {
+    return typeof props.imageGap === 'number' ? px2rpx(props.imageGap) : props.imageGap
+})
+
+const imageBorderRadiusPx = computed(() => {
+    return typeof props.imageBorderRadius === 'number' ? px2rpx(props.imageBorderRadius) : props.imageBorderRadius
+})
+
+// 自定义按钮样式
+const customButtonStyle = computed(() => ({
+    width: imageWidthPx.value,
+    height: imageHeightPx.value,
+    ...props.customStyles.customButton
+}))
+
+// 预览图片样式
+const previewImageStyle = computed(() => ({
+    width: imageWidthPx.value,
+    height: imageHeightPx.value,
+    borderRadius: imageBorderRadiusPx.value,
+    ...props.customStyles.previewImage
+}))
+
+// 图片项样式
+const imageItemStyle = computed(() => ({
+    width: imageWidthPx.value,
+    height: imageHeightPx.value,
+    borderRadius: imageBorderRadiusPx.value,
+    ...props.customStyles.imageItem
+}))
+
+// 列表容器样式
+const listContainerStyle = computed(() => ({
+    gap: imageGapPx.value,
+    ...props.customStyles.listContainer
+}))
+
+// 暂无图片样式
+const noImageStyle = computed(() => ({
+    width: imageWidthPx.value,
+    height: imageHeightPx.value,
+    borderRadius: imageBorderRadiusPx.value,
+    ...props.customStyles.noImage
+}))
+
+const noImageTextStyle = computed(() => props.customStyles.noImageText || {})
+const progressStyle = computed(() => props.customStyles.progress || {})
+const deleteBtnStyle = computed(() => props.customStyles.deleteBtn || {})
+
+// 获取文件值,用于回显
+const getFileValue = (value) => {
+    if (!value) return props.multiple ? [] : []
+
+    if (props.multiple) {
+        if (Array.isArray(value)) {
+            return value.map(item => ({
+                url: getFullUrl(item.path || item),
+                path: item.path || item,
+                name: item.name || '图片',
+                uuid: item.id || item.path || Date.now() + Math.random()
+            }))
+        }
+        return []
+    } else {
+        console.log(value, [{
+            url: getFullUrl(value),
+            path: value,
+            name: '图片',
+            uuid: value
+        }], 121212121);
+
+        if (typeof value === 'string') {
+            return value ? [{
+                url: getFullUrl(value),
+                path: value,
+                name: '图片',
+                uuid: value
+            }] : []
+        }
+        if (value && value.path) {
+            return [{
+                url: getFullUrl(value.path),
+                path: value.path,
+                name: value.name || '图片',
+                uuid: value.id || value.path
+            }]
+        }
+        return []
+    }
+}
+
+// 获取文件列表
+const getFileList = (value) => {
+    if (!value) return []
+
+    if (props.multiple) {
+        return Array.isArray(value) ? value : []
+    } else {
+        if (typeof value === 'string') {
+            return value ? [{ path: value }] : []
+        }
+        return value ? [value] : []
+    }
+}
+
+// 获取完整URL
+const getFullUrl = (path) => {
+    if (!path) return ''
+    if (path.startsWith('http')) return path
+    return (props.baseUrl || config.Host80) + (props.imagePathPrefix || '') + path
+}
+
+// 获取图片URL
+const getImageUrl = (value) => {
+    if (!value) return ''
+    if (typeof value === 'string') return getFullUrl(value)
+    if (value.url) return value.url
+    if (value.path) return getFullUrl(value.path)
+    return ''
+}
+
+// 获取文件路径
+const getFilePath = (file) => {
+    if (!file) return ''
+    if (typeof file === 'string') return file
+    return file.path || file.url || ''
+}
+
+// 获取文件URL(用于预览)
+const getFileUrl = (file) => {
+    if (!file) return ''
+    if (typeof file === 'string') return getFullUrl(file)
+    return getFullUrl(file.path || file.url || '')
+}
+
+// 处理选择文件
+const handleSelect = async (e) => {
+    uploadError.value = ''
+    emit('select', e)
+
+    if (props.autoUpload) {
+        await uploadFiles(e.tempFiles)
+    }
+}
+
+// 上传文件
+const uploadFiles = async (tempFiles) => {
+    if (!tempFiles || tempFiles.length === 0) return
+
+    isUploading.value = true
+    emit('upload-start')
+
+    try {
+        const uploadedPaths = []
+
+        for (let i = 0; i < tempFiles.length; i++) {
+            const file = tempFiles[i]
+            uploadProgress.value = Math.round(((i) / tempFiles.length) * 100)
+
+            const result = await uploadSingleFile(file)
+            if (result.success) {
+                uploadedPaths.push(result.path)
+            } else {
+                throw new Error(result.message)
+            }
+
+            uploadProgress.value = Math.round(((i + 1) / tempFiles.length) * 100)
+        }
+
+        // 更新modelValue
+        if (props.multiple) {
+            // 多张图片:合并新旧数据
+            const currentValue = Array.isArray(props.modelValue) ? [...props.modelValue] : []
+            const newValue = [...currentValue, ...uploadedPaths.map(path => ({ path }))]
+            emit('update:modelValue', newValue)
+        } else {
+            // 单张图片:直接替换
+            const newPath = uploadedPaths[0]
+            console.log(3333, props.modelValue);
+
+            emit('update:modelValue', newPath)
+        }
+
+        uploadProgress.value = 100
+        setTimeout(() => {
+            uploadProgress.value = 0
+        }, 500)
+
+        emit('upload-complete', { success: true, paths: uploadedPaths })
+
+    } catch (error) {
+        uploadError.value = error.message || '上传失败'
+        uploadProgress.value = 0
+        emit('upload-complete', { success: false, error: error.message })
+        uni.showToast({
+            title: error.message || '上传失败',
+            icon: 'none'
+        })
+    } finally {
+        isUploading.value = false
+    }
+}
+
+// 上传单个文件
+const uploadSingleFile = (file) => {
+    return new Promise((resolve, reject) => {
+        const uploadTask = uni.uploadFile({
+            url: config.Host80 + props.uploadUrl,
+            filePath: file.path,
+            name: props.uploadName,
+            header: props.uploadHeaders,
+            name: 'file',
+            header: {
+                'Access-Token': userToken.value
+            },
+            formData: props.uploadData,
+            success: (res) => {
+                const result = props.responseHandler(res.data)
+                if (result.success) {
+                    resolve(result)
+                } else {
+                    reject(new Error(result.message))
+                }
+            },
+            fail: (err) => {
+                console.error('上传失败:', err)
+                reject(new Error('网络错误,请重试'))
+            }
+        })
+        // 监听上传进度
+        uploadTask.onProgressUpdate((res) => {
+            // 这里可以处理单个文件的上传进度
+            console.log('单文件上传进度:', res.progress)
+        })
+    })
+}
+
+// 处理删除文件
+const handleDelete = (e) => {
+    emit('delete', e)
+    // 更新modelValue
+    if (props.multiple) {
+        const currentValue = Array.isArray(props.modelValue) ? [...props.modelValue] : []
+        const newValue = currentValue.filter((_, index) => index !== e.index)
+        emit('update:modelValue', newValue)
+    } else {
+        emit('update:modelValue', '')
+    }
+}
+
+// 处理预览
+const handlePreview = (path) => {
+    if (!path) return
+    const urls = getFileList(props.modelValue).map(f => getFullUrl(getFilePath(f)))
+    uni.previewImage({
+        current: getFullUrl(path),
+        urls: urls.length ? urls : [getFullUrl(path)]
+    })
+    emit('preview', path)
+}
+
+// 处理图片加载错误
+const handleImageError = (e) => {
+    console.error('图片加载失败:', e)
+    emit('image-error', e)
+}
+
+// 处理预览时的删除
+const handlePreviewDelete = (file, index) => {
+    emit('preview-delete', { file, index })
+
+    // 更新modelValue
+    if (props.multiple) {
+        const currentValue = Array.isArray(props.modelValue) ? [...props.modelValue] : []
+        currentValue.splice(index, 1)
+        emit('update:modelValue', currentValue)
+    }
+}
+
+// 单位转换函数
+const px2rpx = (px) => {
+    // 假设设计稿750px,1px = 2rpx
+    return px * 2 + 'rpx'
+}
+
+// 对外暴露方法
+defineExpose({
+    uploadFiles,
+    clearError: () => uploadError.value = ''
+})
+</script>
+
+<style lang="scss" scoped>
+.common-file-uploader {
+    width: 100%;
+
+    .upload-wrapper {
+        width: 100%;
+
+        :deep(.uni-file-picker) {
+            .file-picker__box {
+                border: none;
+                background: transparent;
+            }
+
+            .file-picker__box-list {
+                display: flex;
+                gap: v-bind(imageGapPx);
+                flex-wrap: wrap;
+
+                .file-picker__box-item {
+                    position: relative;
+                    border: 1px solid #e5e7eb;
+                    overflow: hidden;
+
+                    .file-picker__box-item-image {
+                        width: 100%;
+                        height: 100%;
+                        object-fit: cover;
+                    }
+
+                    .file-picker__box-item-close {
+                        position: absolute;
+                        top: 4px;
+                        right: 4px;
+                        width: 24px;
+                        height: 24px;
+                        background: rgba(0, 0, 0, 0.5);
+                        border-radius: 50%;
+                        display: flex;
+                        align-items: center;
+                        justify-content: center;
+
+                        &::before {
+                            content: '×';
+                            color: #fff;
+                            font-size: 28px;
+                            line-height: 1;
+                        }
+
+                        .close-icon {
+                            display: none;
+                        }
+                    }
+                }
+            }
+
+            .file-picker__box-add {
+                margin: 0;
+                border: 2rpx dashed #d1d5db;
+                background: #f9fafb;
+                transition: all 0.3s;
+                cursor: pointer;
+
+                &:hover {
+                    border-color: #ea2027;
+                    background: #fef2f2;
+                }
+
+                .custom-upload-btn {
+                    width: 100%;
+                    height: 100%;
+                    display: flex;
+                    flex-direction: column;
+                    align-items: center;
+                    justify-content: center;
+
+                    &.replace-btn {
+                        background: rgba(0, 0, 0, 0.03);
+
+                        .replace-icon {
+                            font-size: 40px;
+                            color: #666;
+                            margin-bottom: 4px;
+                            transform: rotate(90deg);
+                        }
+
+                        .tip {
+                            color: #666;
+                        }
+
+                        &:hover {
+                            background: rgba(234, 32, 39, 0.05);
+
+                            .replace-icon,
+                            .tip {
+                                color: #ea2027;
+                            }
+                        }
+                    }
+
+                    .plus {
+                        font-size: 48px;
+                        color: #9ca3af;
+                        line-height: 1;
+                        margin-bottom: 8px;
+                    }
+
+                    .tip {
+                        font-size: 24px;
+                        color: #6b7280;
+                    }
+                }
+
+                .add-icon {
+                    display: none;
+                }
+            }
+
+            .file-picker__box-progress {
+                border-radius: v-bind(imageBorderRadiusPx);
+                background: rgba(0, 0, 0, 0.5);
+                color: #fff;
+            }
+        }
+
+        .upload-progress {
+            margin-top: 8px;
+            padding: 8px;
+            background: #f5f5f5;
+            border-radius: 4px;
+            position: relative;
+
+            .progress-bar {
+                height: 4px;
+                background: #ea2027;
+                border-radius: 2px;
+                transition: width 0.3s;
+            }
+
+            .progress-text {
+                position: absolute;
+                right: 8px;
+                top: 8px;
+                font-size: 24px;
+                color: #666;
+            }
+        }
+
+        .upload-error {
+            margin-top: 8px;
+            padding: 8px;
+            background: #fee2e2;
+            border-radius: 4px;
+
+            .error-text {
+                color: #dc2626;
+                font-size: 24px;
+            }
+        }
+    }
+
+    .image-preview {
+        .image-list {
+            display: flex;
+            gap: v-bind(imageGapPx);
+            flex-wrap: wrap;
+
+            .image-item {
+                position: relative;
+                overflow: hidden;
+                border: 1px solid #e5e7eb;
+
+                .preview-image {
+                    width: 100%;
+                    height: 100%;
+                    cursor: pointer;
+                }
+
+                .preview-delete-btn {
+                    position: absolute;
+                    top: 4px;
+                    right: 4px;
+                    width: 24px;
+                    height: 24px;
+                    background: rgba(0, 0, 0, 0.5);
+                    border-radius: 50%;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    cursor: pointer;
+
+                    .delete-icon {
+                        color: #fff;
+                        font-size: 28px;
+                        line-height: 1;
+                    }
+                }
+            }
+        }
+
+        .single-preview {
+            .preview-image {
+                cursor: pointer;
+                border: 1px solid #e5e7eb;
+            }
+        }
+
+        .no-image {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            background: #f5f5f5;
+            border: 1px solid #e5e7eb;
+
+            text {
+                color: #999;
+                font-size: 24px;
+            }
+        }
+    }
+}
+</style>

+ 169 - 0
components/cwg-file.vue

@@ -0,0 +1,169 @@
+<template>
+    <view class="file-preview">
+        <!-- PDF 预览链接 -->
+        <view v-if="isPdfFile" class="pdf-link" @click="handlePreviewPdf">
+            <uni-icons type="document" size="20" color="#ff0000"></uni-icons>
+            <text class="pdf-text">{{ displayFileName }}</text>
+        </view>
+
+        <!-- 图片预览 -->
+        <view v-else-if="isImageFile" class="image-preview">
+            <image :src="fullPath" mode="aspectFill" class="preview-image" @click="handlePreviewImage" />
+        </view>
+
+        <!-- 其他文件类型 -->
+        <view v-else class="file-info">
+            <uni-icons type="folder" size="20" color="#007aff"></uni-icons>
+            <text class="file-name">{{ displayFileName }}</text>
+        </view>
+    </view>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import config from '@/config';
+const props = defineProps({
+    // 文件路径
+    path: {
+        type: String,
+        default: ''
+    },
+    // 更新URL前缀
+    updateUrl: {
+        type: String,
+        default: config.Host80
+    },
+    // 文件名称(可选)
+    fileName: {
+        type: String,
+        default: ''
+    }
+})
+
+// 完整的文件路径
+const fullPath = computed(() => {
+    console.log(props.updateUrl, props.path);
+    
+    if (!props.path) return ''
+    return props.updateUrl + props.path
+})
+
+// 判断是否为PDF文件
+const isPdfFile = computed(() => {
+    if (!props.path) return false
+    const ext = props.path.substr(-3).toLowerCase()
+    return ext === 'pdf'
+})
+
+// 判断是否为图片文件
+const isImageFile = computed(() => {
+    if (!props.path) return false
+    const ext = props.path.substr(-3).toLowerCase()
+    return ['jpg', 'png', 'gif', 'jpeg', 'bmp', 'webp'].includes(ext) ||
+        props.path.substr(-4).toLowerCase() === 'jpeg'
+})
+
+// 获取文件名
+const displayFileName = computed(() => {
+    if (props.fileName) return props.fileName
+    if (!props.path) return '--'
+
+    // 提取文件名
+    const parts = props.path.split('/')
+    return parts[parts.length - 1] || props.path
+})
+
+// 预览PDF
+const handlePreviewPdf = () => {
+    if (!fullPath.value) return
+
+    // #ifdef H5
+    window.open(fullPath.value, '_blank')
+    // #endif
+
+    // #ifdef APP-PLUS
+    plus.runtime.openURL(fullPath.value)
+    // #endif
+
+    // #ifdef MP-WEIXIN
+    uni.downloadFile({
+        url: fullPath.value,
+        success: (res) => {
+            if (res.statusCode === 200) {
+                uni.openDocument({
+                    filePath: res.tempFilePath,
+                    success: () => {
+                        console.log('打开文档成功')
+                    }
+                })
+            }
+        }
+    })
+    // #endif
+}
+
+// 预览图片
+const handlePreviewImage = () => {
+    if (!fullPath.value) return
+
+    uni.previewImage({
+        urls: [fullPath.value],
+        current: 0
+    })
+}
+</script>
+
+<style scoped lang="scss">
+.pdf-link {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(8);
+    padding: px2rpx(4) px2rpx(8);
+    background-color: #fff1f0;
+    border-radius: px2rpx(4);
+    cursor: pointer;
+    transition: all 0.3s;
+
+    &:hover {
+        background-color: #ffe7e6;
+    }
+
+    .pdf-text {
+        color: #ff0000;
+        font-size: px2rpx(14);
+        max-width: px2rpx(200);
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+    }
+}
+
+.image-preview {
+    .preview-image {
+        width: px2rpx(60);
+        height: px2rpx(60);
+        border-radius: px2rpx(4);
+        object-fit: cover;
+        cursor: pointer;
+        transition: transform 0.3s;
+
+        &:hover {
+            transform: scale(1.1);
+        }
+    }
+}
+
+.file-info {
+    display: flex;
+    align-items: center;
+
+    .file-name {
+        color: #666;
+        font-size: px2rpx(14);
+        max-width: px2rpx(200);
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+    }
+}
+</style>

+ 1 - 1
components/cwg-header.vue

@@ -43,7 +43,7 @@ function getTranslationKey(): string {
   let key = ''
   if (!path) return ''
   if (path === '/') {
-    key = 'pages.card.index'
+    key = 'pages.mine.info'
   } else if (path.startsWith()) {
     key = path.slice(7)
   } else {

+ 522 - 0
components/cwg-input1.vue

@@ -0,0 +1,522 @@
+<template>
+  <view class="form-group" :ref="setItemRef">
+    <view v-if="label" class="form-label"><text class="required-mark">{{ required ? "*" : "" }}</text> {{ label }}
+    </view>
+    <u-form-item :prop="rulesKey">
+      <template v-if="type === 'text' || type === 'password'">
+        <up-input v-model="inputValueDoc" class="form-input" :type="type"
+          :placeholder="placeholder ? placeholder : t('common.input')" :readonly="readonly" :disabled="disabled"
+          :clearable="clearable" :maxlength="maxlength" :errorMessage="errorMessage" @blur="handleBlur"
+          @focus="handleFocus" @clear="handleClear">
+          <template #prefix>
+            <slot name="left-icon1"></slot>
+          </template>
+          <template #suffix>
+            <slot name="right-icon1"></slot>
+          </template>
+        </up-input>
+      </template>
+      <template v-if="type === 'number'">
+        <up-input v-model="inputValueDoc" class="form-input" type="number"
+          :placeholder="placeholder ? placeholder : t('common.input')" :readonly="readonly" :disabled="disabled"
+          :clearable="clearable" :maxlength="maxlength" :errorMessage="errorMessage" @blur="handleBlur"
+          @focus="handleFocus" @clear="handleClear">
+          <slot></slot>
+        </up-input>
+      </template>
+      <template v-if="type === 'dropdown'">
+        <view class="input-wrapper" @click.stop="handleInputClick">
+          <view class="input-bg"></view>
+          <up-input v-model="inputValueDoc" class="form-input"
+            :placeholder="placeholder ? placeholder : t('common.choose')" :readonly="true" :disabled="disabled"
+            :clearable="clearable" :errorMessage="errorMessage" @clear="handleClear">
+            <template #suffix>
+              <up-icon name="arrow-down" size="14"></up-icon>
+            </template>
+            <slot></slot>
+          </up-input>
+        </view>
+        <up-action-sheet :show="filteredColumns.length && showPicker" :actions="filteredColumns"
+          @select="onActionSheetConfirm" @close="showPicker = false" />
+      </template>
+      <template v-if="type === 'select'">
+        <view class="input-wrapper" @click.stop="handleInputClick">
+          <view class="input-bg"></view>
+          <up-input v-model="inputValueDoc" class="form-input"
+            :placeholder="placeholder ? placeholder : t('common.choose')" :readonly="true" :disabled="disabled"
+            :clearable="clearable" :errorMessage="errorMessage" @clear="handleClear">
+            <template #suffix>
+              <up-icon name="arrow-down" size="14"></up-icon>
+            </template>
+            <slot></slot>
+          </up-input>
+        </view>
+        <!-- :showSearch="false" :options="selectPopup.options" v-model="selectPopup.show" @select="onSelectConfirm" -->
+        <cwg-more-select v-if="showPicker" :showSearch="showSearch" :input-value="inputValueDoc"
+          :options="filteredColumns" v-model="showPicker" @select="onConfirm" />
+      </template>
+      <template v-if="type === 'date'">
+        <view class="input-wrapper" @click="handleInputClick">
+          <view class="input-bg"></view>
+          <up-input v-model="inputValueDoc" class="form-input"
+            :placeholder="placeholder ? placeholder : t('common.choose')" :readonly="true" :disabled="disabled"
+            :clearable="clearable" :errorMessage="errorMessage" @clear="handleClear">
+            <template #suffix>
+              <up-icon name="calendar" size="14"></up-icon>
+            </template>
+          </up-input>
+        </view>
+        <cwg-date-picker v-model:show="showPicker" v-model="inputValueDoc" mode="date" @confirm="onDateConfirm"
+          :minDate="minDate" :maxDate="maxDate" />
+      </template>
+      <template v-if="type === 'upload'">
+        <view class="form-input uploader">
+          <up-upload v-if="!isUploadD" :fileList="uploader" :disabled="disabled" :deletable="!disabled" :accept="accept"
+            :maxCount="1" @afterRead="afterRead" @delete="handleDelete"></up-upload>
+          <up-upload v-if="isUploadD" :fileList="uploader" :disabled="disabled" :deletable="!disabled" :maxCount="1"
+            @afterRead="afterRead" @delete="handleDelete">
+            <slot></slot>
+          </up-upload>
+        </view>
+      </template>
+    </u-form-item>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted, watch, computed, defineProps } from "vue";
+import dayjs from "dayjs";
+import { upload } from "@/utils/request";
+import { uploadApi } from "@/api/upload";
+import config from "@/config";
+import { useI18n } from "vue-i18n";
+
+const { t } = useI18n();
+const props = defineProps({
+  type: {
+    type: String,
+    default: "text",
+    validator: (v) =>
+      [
+        "text",
+        "password",
+        "number",
+        "select",
+        "date",
+        "dropdown",
+        "upload",
+      ].includes(v),
+  },
+  label: String,
+  fkey: String,
+  rulesKey: String,
+  accept: String,
+  showSearch: Boolean,
+  isUploadD: { type: Boolean, default: false },
+  value: { type: [String, Number] },
+  selectedValueDoc: { type: [String, Number] },
+  placeholder: String,
+  direction: { type: String, default: "down" },
+  disabled: Boolean,
+  readonly: Boolean,
+  required: Boolean,
+  max: Number,
+  clearable: { type: Boolean, default: true },
+  columns: { type: Array, default: () => [] },
+  rules: { type: Array, default: () => [] },
+  maxlength: Number,
+  errorMessage: String,
+  minDate: { type: Date, default: () => new Date(1920, 0, 1).getTime() },
+  maxDate: { type: Date, default: () => new Date().getTime() },
+  dateFormatter: { type: Function, default: (_type, val) => val },
+  displayFormatter: {
+    type: Function,
+    default: (val) => dayjs(val).format("YYYY-MM-DD"),
+  },
+});
+//  console.log(props, 'props');
+const itemRef = ref(null)
+const setItemRef = el => {
+  if (el) itemRef.value = el
+}
+const emit = defineEmits([
+  "update:value",
+  "blur",
+  "focus",
+  "clear",
+  "confirm",
+  "change",
+  "change1",
+]);
+
+const isUploading = ref(false);
+const uploader = ref([]);
+const inputValueDoc = ref("");
+const selectedValueDoc = ref([]);
+const showPicker = ref(false);
+const onConfirm = (value) => {
+  let selectedText = value.text || ''
+  if (props.fkey == 'areaCode') {
+    selectedText = selectedText
+  }
+  inputValueDoc.value = selectedText
+  showPicker.value = false
+  emit("update:selectedValueDoc", selectedText);
+}
+// 处理输入框点击事件
+function handleInputClick() {
+  if (props.disabled || props.readonly) return;
+  showPicker.value = true;
+}
+
+function absoluteUrl(url) {
+  if (!url) {
+    return "";
+  }
+  if (/^https?:\/\//.test(url)) {
+    return url;
+  }
+  const prefix = config.Host85 || "";
+  if (!prefix) {
+    return url;
+  }
+  return `${prefix}${url.startsWith("/") ? url : `/${url}`}`;
+}
+
+async function afterRead(file) {
+  isUploading.value = true;
+  uni.showLoading({
+    title: "上传中...",
+    mask: true,
+  });
+  try {
+    //  console.log(file.file, file);
+
+    const result = await uploadApi.uploadFile(file.file);
+    //  console.log(500, result);
+    uni.hideLoading();
+    if (result.code === 200) {
+      inputValueDoc.value = result.data;
+      uploader.value = [{ url: absoluteUrl(result.data) }];
+      uni.showToast({
+        title: "上传成功",
+        icon: "success",
+      });
+      setTimeout(() => {
+        isUploading.value = false;
+      }, 100);
+    } else {
+      uni.showToast({
+        title: result.message || "上传失败",
+        icon: "none",
+      });
+      isUploading.value = false;
+      inputValueDoc.value = "";
+      uploader.value = [];
+    }
+  } catch (_error) {
+    uni.hideLoading();
+    uni.showToast({
+      title: _error.message || "上传失败",
+      icon: "none",
+    });
+  }
+}
+
+async function afterRead1(event) {
+  //  console.log(event, 198);
+
+  isUploading.value = true;
+  uni.showLoading({
+    title: "上传中...",
+    mask: true,
+  });
+  try {
+    // uview-plus 的 upload 组件返回的是 { file, fileList }
+    // file 对象包含 url(临时路径)或 path 属性
+    const file = event.file || (Array.isArray(event) ? event[0] : event);
+    const filePath = file.url || file.path || file.tempFilePath;
+
+    if (!filePath) {
+      throw new Error("文件路径不存在");
+    }
+
+    // 使用 uni-app 的 upload 方法,需要传入 filePath
+    const result = await upload({
+      url: "/wasabi/api/upload/file",
+      filePath: filePath,
+    });
+
+    uni.hideLoading();
+    //  console.log(500, result);
+    if (result.code === 200) {
+      inputValueDoc.value = result.data;
+      uploader.value = [{ url: absoluteUrl(result.data) }];
+      uni.showToast({
+        title: "上传成功",
+        icon: "success",
+      });
+      setTimeout(() => {
+        isUploading.value = false;
+      }, 100);
+    } else {
+      uni.showToast({
+        title: result.message || "上传失败",
+        icon: "none",
+      });
+      isUploading.value = false;
+      inputValueDoc.value = "";
+      uploader.value = [];
+    }
+  } catch (_error) {
+    uni.hideLoading();
+    uni.showToast({
+      title: _error.message || "上传失败",
+      icon: "none",
+    });
+    isUploading.value = false;
+    uploader.value = [];
+  }
+}
+
+function handleDelete(event) {
+  const index = event.index;
+  uploader.value.splice(index, 1);
+  inputValueDoc.value = "";
+  emit("update:value", "");
+  emit("change", { value: "", key: props.fkey });
+}
+
+const filteredColumns = computed(() => {
+  if (props.type === "dropdown") {
+    return props.columns.map((item) => {
+      return { ...item, name: item.text };
+    });
+  }
+  return props.columns;
+});
+
+watch(
+  () => inputValueDoc.value,
+  (newVal) => {
+    // if (!newVal) return
+    if (
+      props.type === "text" ||
+      props.type === "number" ||
+      props.type === "password"
+    ) {
+      emit("update:value", newVal);
+      emit("change", { value: newVal, key: props.fkey });
+    } else if (props.type === "select") {
+      const matched = props.columns.find((opt) => opt.text === newVal);
+      emit("update:value", matched?.value || "");
+      emit("change", { value: matched?.value || "", key: props.fkey });
+    } else if (props.type === "date") {
+      emit("update:value", newVal);
+      emit("change", { value: newVal, key: props.fkey });
+    } else if (props.type === "upload") {
+      emit("update:value", newVal);
+      emit("change", { value: newVal, key: props.fkey });
+    } else if (props.type === "dropdown") {
+      const matched = props.columns.find((opt) => opt.text === newVal);
+      emit("update:value", matched?.value || "");
+      emit("change", { value: matched?.value || "", key: props.fkey });
+    }
+  }
+);
+
+watch(
+  () => props.value,
+  (newVal) => {
+    if (isUploading.value) return;
+    if (props.type === "date") {
+      inputValueDoc.value = newVal ? dayjs(newVal).format("YYYY-MM-DD") : "";
+    } else if (props.type === "select") {
+      const matched = props.columns.find((opt) => opt.value === newVal);
+      inputValueDoc.value = matched?.text || "";
+      selectedValueDoc.value = matched ? [matched.value] : [];
+    } else if (props.type === "upload") {
+      uploader.value = props.value
+        ? [{ url: absoluteUrl(String(props.value)) }]
+        : [];
+      //  console.log(uploader.value, 198);
+      inputValueDoc.value = props.value || "";
+    } else if (props.type === "dropdown") {
+      const matched = props.columns.find((opt) => opt.value === newVal);
+      inputValueDoc.value = matched?.text || "";
+      selectedValueDoc.value = matched ? [matched.value] : [];
+    } else {
+      inputValueDoc.value = newVal || "";
+    }
+  },
+  { immediate: true }
+);
+
+function handleBlur(event) {
+  emit("blur", event);
+}
+
+function handleFocus(event) {
+  emit("focus", event);
+}
+
+function handleClear() {
+  inputValueDoc.value = "";
+  emit("update:value", "");
+  emit("clear");
+}
+
+function onActionSheetConfirm(value) {
+  const selectedText = value.text || value.name || "";
+  inputValueDoc.value = selectedText;
+  showPicker.value = false;
+  const matched = props.columns.find(
+    (opt) => opt.text === selectedText || opt.name === selectedText
+  );
+  emit("update:value", matched?.value || "");
+  emit("change", { value: matched?.value || "", key: props.fkey });
+}
+
+function onPickerConfirm(event) {
+  const { value } = event;
+  const selectedItem =
+    Array.isArray(value) && value.length > 0 ? value[0] : value;
+  const matched = props.columns.find(
+    (opt) => opt.value === selectedItem || opt.value === selectedItem?.value
+  );
+  if (matched) {
+    inputValueDoc.value = matched.text;
+    emit("update:value", matched.value);
+    emit("change", { value: matched.value, key: props.fkey });
+  }
+  showPicker.value = false;
+}
+
+function onDateConfirm(event) {
+  const { formatted } = event;
+  inputValueDoc.value = formatted;
+  showPicker.value = false;
+  emit("update:value", formatted);
+  emit("change", { value: formatted, key: props.fkey });
+}
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+
+.form-group {
+  width: 100%;
+  margin-bottom: px2rpx(12);
+  transform: none;
+}
+
+.form-label {
+  font-size: var(--font-size-16);
+  line-height: px2rpx(24);
+  letter-spacing: px2rpx(1);
+  color: #474747;
+  margin: px2rpx(16) 0 px2rpx(8) 0;
+  display: flex;
+  align-self: start;
+
+  .required-mark {
+    color: red;
+  }
+}
+
+.form-input {
+  width: 100%;
+  border: 1px solid var(--border) !important;
+  border-radius: 10px !important;
+  font-size: px2rpx(31) !important;
+  margin-top: px2rpx(4) !important;
+  padding: px2rpx(10) px2rpx(12) !important;
+  box-sizing: border-box;
+}
+
+.search-field {
+  background: var(--black);
+  border: none !important;
+  padding-left: px2rpx(10) !important;
+}
+
+.uploader {
+  border: none !important;
+  background: transparent !important;
+  padding-left: 0;
+  padding-right: 0;
+  width: 100%;
+}
+
+.input-wrapper {
+  position: relative;
+  width: 100%;
+
+  .input-bg {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    z-index: 100;
+  }
+}
+
+// uview-plus 组件样式覆盖
+:deep(.u-input) {
+  border-radius: 10px !important;
+  font-size: px2rpx(31) !important;
+  padding: px2rpx(12);
+  background: transparent;
+  color: var(--white);
+
+  &::after {
+    border: none !important;
+  }
+}
+
+:deep(.up-input__content__field-wrapper__field) {
+  color: var(--white);
+  line-height: px2rpx(22);
+
+  &::placeholder {
+    color: var(--lable) !important;
+  }
+}
+
+: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%;
+
+  >view {
+    width: 100% !important;
+  }
+
+  .u-upload__wrap__preview__image {
+    width: 100% !important;
+    height: px2rpx(160) !important;
+    border-radius: px2rpx(4);
+  }
+
+  .u-upload__deletable {
+    width: px2rpx(24) !important;
+    height: px2rpx(24) !important;
+    background-color: rgba(0, 0, 0, 0.6) !important;
+  }
+
+  .u-icon__icon {
+    width: px2rpx(18) !important;
+    height: px2rpx(18) !important;
+    font-size: px2rpx(18) !important;
+  }
+}
+</style>

+ 106 - 66
components/cwg-page-wrapper.vue

@@ -9,100 +9,140 @@
     <cwg-progress />
     <view class="page-content">
       <cwg-match-media :min-width="991" v-if="!isLoginPage">
-        <cwg-pc-header @toggle-sidebar="toggleSidebar" />
+        <cwg-pc-header @toggle-sidebar="toggleSidebar" :sidebarVisible="sidebarVisible"
+          @open-right-drawer="openRightDrawer" />
       </cwg-match-media>
       <cwg-match-media :max-width="991">
-        <cwg-header v-if="!isHeaderFixed" />
+        <cwg-header v-if="!isHeaderFixed">
+          <view class="mobile-menu-btn" @click="openRightDrawer">
+            <cwg-icon name="crm-bars-staggered" :size="20" color="#0f172b" />
+          </view>
+        </cwg-header>
       </cwg-match-media>
-      <view class="content-wrapper" :class="{ 'content-wrapper-padding': isContentPadding}">
+      <view class="content-wrapper" :class="{ 'content-wrapper-padding': isContentPadding }">
         <slot />
       </view>
       <view :style="{ height: isTabBarPage ? '60px' : '0px' }" />
     </view>
     <cwg-match-media :max-width="991">
       <cwg-tab-bar v-model:isTabBarPage="isTabBarPage" />
+
     </cwg-match-media>
+    <cwg-right-drawer v-if="!isLoginPage" ref="rightDrawerRef" @navigate="handleDrawerNavigate"
+      @logout="handleDrawerLogout" />
   </view>
 </template>
 
 <script setup lang="ts">
-  import { ref, watch, computed } from "vue";
-  import { onLoad, onShow } from '@dcloudio/uni-app'
-  import { updateRoute } from '@/hooks/useRoute'
-  import useGlobalStore from '@/stores/use-global-store'
-  const globalStore = useGlobalStore()
-  interface MenuItem {
-    key: string;
-    label: string;
-    icon?: string;
-    children?: MenuItem[];
+import { ref, computed } from "vue";
+import { onLoad, onShow } from '@dcloudio/uni-app'
+import { updateRoute } from '@/hooks/useRoute'
+import useRouter from '@/hooks/useRouter'
+import useUserStore from '@/stores/use-user-store'
+import { userApi } from '@/api/user'
+import useGlobalStore from '@/stores/use-global-store'
+const globalStore = useGlobalStore()
+const router = useRouter()
+const userStore = useUserStore()
+
+const props = defineProps({
+  // 是否固定顶部导航栏
+  isHeaderFixed: {
+    type: Boolean,
+    default: false
+  },
+  // 是否登录页,登录页不显示侧边栏和顶部导航,注册忘记密码等页面
+  isLoginPage: {
+    type: Boolean,
+    default: false
+  },
+  // 是否含有padding 默认有
+  isContentPadding: {
+    type: Boolean,
+    default: true
   }
+})
+const isDark = computed(() => globalStore.theme === 'dark')
+const isTabBarPage = ref(false)
 
-  const props = defineProps({
-    // 是否固定顶部导航栏
-    isHeaderFixed: {
-      type: Boolean,
-      default: false
-    },
-    // 是否登录页,登录页不显示侧边栏和顶部导航,注册忘记密码等页面
-    isLoginPage: {
-      type: Boolean,
-      default: false
-    },
-    // 是否含有padding 默认有
-    isContentPadding: {
-      type: Boolean,
-      default: true
-    }
-  })
-  const isDark = computed(() => globalStore.theme === 'dark')
-  const isTabBarPage = ref(false)
 
+const sidebarVisible = ref(true)
+const rightDrawerRef = ref<any>(null)
+
+function toggleSidebar() {
+  sidebarVisible.value = !sidebarVisible.value;
+}
 
-  const sidebarVisible = ref(true)
+function openRightDrawer() {
+  console.log(32423423);
 
-  function toggleSidebar() {
-    sidebarVisible.value = !sidebarVisible.value;
+  rightDrawerRef.value?.open()
+}
+
+function handleDrawerNavigate(path: string) {
+  router.push(path)
+}
+
+async function handleDrawerLogout() {
+  try {
+    const res = await userApi.logout()
+    if (res.code === 200) {
+      userStore.clearUserInfo()
+      router.push('/pages/login/index')
+    }
+  } catch (error) {
+    userStore.clearUserInfo()
+    router.push('/pages/login/index')
   }
+}
 
-  onLoad(() => {
-    updateRoute()
-  })
+onLoad(() => {
+  updateRoute()
+})
 
-  onShow(() => {
-    updateRoute()
-  })
+onShow(() => {
+  updateRoute()
+})
 
 </script>
 
 <style lang="scss" scoped>
-  @import "@/uni.scss";
+@import "@/uni.scss";
 
-  .page-wrapper {
-    display: flex;
-    flex-direction: row;
-    height: 100vh;
-    overflow: hidden;
-  }
+.page-wrapper {
+  display: flex;
+  flex-direction: row;
+  height: 100vh;
+  overflow: hidden;
+}
 
-  .left-sidebar {
-    display: flex;
-    flex-direction: row;
-  }
+.left-sidebar {
+  display: flex;
+  flex-direction: row;
+}
 
-  .page-content {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-  }
+.page-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
 
-  .content-wrapper {
-    flex: 1;
-    // padding: 0 px2rpx(24) px2rpx(0) px2rpx(24);
-    box-sizing: border-box;
-    overflow-y: auto;
-  }
-  .content-wrapper-padding{
-    padding: px2rpx(30);
-  }
+.content-wrapper {
+  flex: 1;
+  // padding: 0 px2rpx(24) px2rpx(0) px2rpx(24);
+  box-sizing: border-box;
+  overflow-y: auto;
+}
+
+.content-wrapper-padding {
+  padding: px2rpx(30);
+}
+
+.mobile-menu-btn {
+  width: px2rpx(64);
+  height: px2rpx(64);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
 </style>

+ 15 - 4
components/cwg-pc-header.vue

@@ -1,7 +1,8 @@
 <template>
     <header class="cwg-pc-header">
         <div class="left">
-            <text class="back-arrow" @click="$emit('toggle-sidebar')">&#8592;</text>
+            <cwg-icon :name="!props.sidebarVisible ? 'crm-bars-staggered' : 'crm-angle-left'" :size="24" color="#e32326"
+                @click="$emit('toggle-sidebar')" />
         </div>
         <div class="center"></div>
         <div class="right">
@@ -15,7 +16,7 @@
             <text class="icon avatar">
                 <image class="img" src="/static/avatar.png" alt="avatar" />
             </text>
-            <text class="icon logo" @click="goUserInfo">
+            <text class="icon logo" @click="openRightDrawer">
                 <image class="img" src="/static/logo.png" alt="logo" />
             </text>
         </div>
@@ -27,12 +28,22 @@ import useGlobalStore from '@/stores/use-global-store'
 const globalStore = useGlobalStore()
 import useRouter from "@/hooks/useRouter";
 const router = useRouter();
+const props = defineProps({
+    sidebarVisible: {
+        type: Boolean,
+        default: false
+    },
+})
+const emit = defineEmits<{
+    (e: 'open-right-drawer'): void
+}>()
+
 function toggleTheme() {
     globalStore.setGlobalTheme(globalStore.theme === 'light' ? 'dark' : 'light');
 }
 
-function goUserInfo() {
-    router.push('/pages/mine/info')
+function openRightDrawer() {
+    emit('open-right-drawer');
 }
 </script>
 

+ 171 - 0
components/cwg-right-drawer.vue

@@ -0,0 +1,171 @@
+<template>
+    <uni-popup ref="popupRef" type="right" background-color="#f5f5f5">
+        <view class="right-drawer">
+            <view class="drawer-header">
+                <image class="avatar" src="/static/logo.png" mode="aspectFill" />
+                <view class="user-info">
+                    <text class="name">{{ displayName }}</text>
+                    <text class="cid">CID: {{ displayCid }}</text>
+                </view>
+            </view>
+
+            <view class="menu-list">
+                <view v-for="item in menuList" :key="item.key" class="menu-item"
+                    :class="{ active: activePath === item.path }" @click="handleNavigate(item.path)">
+                    <cwg-icon :name="item.icon" :size="16" :color="activePath === item.path ? '#fff' : '#0f172b'" />
+                    <text>{{ item.name }}</text>
+                </view>
+            </view>
+            <view class="logout-wrap">
+                <view class="logout-btn" @click="handleLogout">
+                    <cwg-icon name="logout" :size="16" color="#ff9800" />
+                    <text>Logout</text>
+                </view>
+            </view>
+        </view>
+    </uni-popup>
+</template>
+
+<script setup lang="ts">
+import { computed, ref } from 'vue'
+import useRoute from '@/hooks/useRoute'
+import useUserStore from '@/stores/use-user-store'
+import { useI18n } from "vue-i18n";
+const { t } = useI18n();
+const emit = defineEmits<{
+    (e: 'navigate', path: string): void
+    (e: 'logout'): void
+}>()
+
+const popupRef = ref<any>(null)
+const userStore = useUserStore()
+const userInfo = computed<any>(() => userStore.userInfo || {})
+const route = useRoute()
+const menuList = computed(() => [
+    {
+        id: 1, path: '/pages/mine/info?type=1', name: t('PersonalManagement.Title.PersonalInformation'), icon: 'crm-circle-user'
+    },
+    {
+        id: 2, path: '/pages/mine/info?type=2', name: t('PersonalManagement.Title.BankInformation'), icon: 'crm-building-columns'
+    },
+    {
+        id: 3, path: '/pages/mine/info?type=3', name: t('PersonalManagement.Title.FileManagement'), icon: 'crm-file'
+    },
+    {
+        id: 4, path: '/pages/mine/info?type=4', name: t('PersonalManagement.Title.SecurityCenter'), icon: 'crm-lock'
+    }
+]);
+
+const displayName = computed(() => {
+    const fullName = `${userInfo.value?.firstName || ''} ${userInfo.value?.lastName || ''}`.trim()
+    return fullName || userInfo.value?.name || userInfo.value?.email || '--'
+})
+
+const displayCid = computed(() => userInfo.value?.cId || userInfo.value?.id || '--')
+const activePath = computed(() => route.path + (route.query?.type ? `?type=${route.query.type}` : '') || '')
+
+function open() {
+    popupRef.value?.open()
+}
+
+function close() {
+    popupRef.value?.close()
+}
+
+function handleNavigate(path: string) {
+    emit('navigate', path)
+    close()
+}
+
+function handleLogout() {
+    emit('logout')
+    close()
+}
+
+defineExpose({
+    open,
+    close
+})
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.right-drawer {
+    width: 300px;
+    height: 100vh;
+    background: #f5f5f5;
+    display: flex;
+    flex-direction: column;
+    padding: 20px 16px;
+}
+
+.drawer-header {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    padding: 20px 16px;
+    border-bottom: 1px solid #d9dde5;
+}
+
+.avatar {
+    width: 76px;
+    height: 76px;
+    border-radius: 12px;
+    background: #fff;
+}
+
+.user-info {
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+}
+
+.name {
+    font-size: 20px;
+    font-weight: 600;
+    color: #334155;
+}
+
+.cid {
+    font-size: 14px;
+    color: #ef4444;
+}
+
+.menu-list {
+    padding: 12px 0;
+}
+
+.menu-item {
+    height: 48px;
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    padding: 0 16px;
+    color: #0f172b;
+    font-size: 16px;
+    font-weight: 600;
+}
+
+.menu-item.active {
+    background: #ea2027;
+    color: #fff;
+}
+
+.logout-wrap {
+    margin-top: auto;
+    padding: 20px 16px;
+    margin-bottom: 20px;
+}
+
+.logout-btn {
+    height: 44px;
+    background: #f4eadf;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    color: #ff9800;
+    font-weight: 600;
+}
+</style>

+ 9 - 8
components/cwg-sidebar.vue

@@ -3,7 +3,7 @@
         <image class="logo" src="/static/images/logo3.png" mode="widthFix" />
         <view class="menu">
             <view class="menu-item" v-for="item in menu" :key="item.key" @click="handleClick(item)">
-                <cwg-icon color="#fff" icon="chevron-right" class="arrow" />
+                <cwg-icon color="#fff" :icon="item.icon" class="arrow" />
             </view>
         </view>
     </view>
@@ -21,10 +21,11 @@ interface MenuItem {
 }
 const emit = defineEmits(['menu-click']);
 const menu = ref<MenuItem[]>([
-    { key: 'client', label: 'Client Zone', icon: 'icon-client' },
-    { key: 'promotion', label: 'Promotion Center', icon: 'icon-promotion' },
-    { key: 'deposit', label: 'Deposit', icon: 'icon-deposit' },
-    { key: 'withdrawal', label: 'Withdrawal', icon: 'icon-withdrawal' },
+    { key: 'client', label: 'Client Zone', icon: 'crm-house' },
+    { key: 'promotion', label: 'Promotion Center', icon: 'crm-users' },
+    { key: 'deposit', label: 'Deposit', icon: 'crm-chart-area' },
+    { key: 'withdrawal', label: 'Withdrawal', icon: 'crm-download' },
+    { key: 'withdrawal', label: 'Withdrawal', icon: 'crm-headset' },
 ]);
 function handleClick(item: MenuItem) {
     router.push(item.key);
@@ -36,7 +37,7 @@ function handleClick(item: MenuItem) {
 
 .cwg-sidebar {
     width: px2rpx(100);
-    background: #11224d;
+    background: var(--color-navy-900);
     color: #fff;
     height: 100vh;
     display: flex;
@@ -65,11 +66,11 @@ function handleClick(item: MenuItem) {
         height: px2rpx(54);
         border-radius: px2rpx(12);
         cursor: pointer;
-        background: #122D6B;
+        background: var(--color-navy-700);
         transition: background 0.2s;
 
         &:hover {
-            background: #24325e;
+            background: var(--color-navy-600);
         }
 
     }

+ 662 - 0
components/cwg-tabel.vue

@@ -0,0 +1,662 @@
+<template>
+    <view class="table-container">
+        <!-- 表格 -->
+        <uni-table :type="selectionType" :border="false" @selection-change="handleSelectionChange">
+            <uni-tr class="tabel-header">
+                <uni-th v-for="column in columns" :key="column.prop" :align="column.align || 'center'"
+                    :width="column.width || '50'">
+                    {{ column.label }}
+                </uni-th>
+                <uni-th v-if="showOperation" align="center" width="200">操作</uni-th>
+            </uni-tr>
+
+            <uni-tr v-for="(row, rowIndex) in tableData" :key="rowIndex">
+                <uni-td v-for="column in columns" :key="column.prop" :align="column.align || 'center'">
+                    <!-- 自定义渲染插槽 -->
+                    <slot v-if="column.slot" :name="column.slot" :row="row" :column="column" :index="rowIndex">
+                        {{ row[column.prop] }}
+                    </slot>
+
+                    <!-- 标签类型渲染 -->
+                    <uni-tag v-else-if="column.type === 'tag'" :text="formatTagText(row[column.prop], column)"
+                        :type="formatTagType(row[column.prop], column)" size="small" />
+
+                    <!-- 文件类型渲染 -->
+                    <view v-else-if="column.type === 'file'">
+                        <cwg-file :path="row[column.prop]" />
+                    </view>
+
+                    <!-- 默认文本渲染 -->
+                    <template v-else>{{ formatCellValue(row[column.prop], column, row) }}</template>
+                </uni-td>
+
+                <!-- 操作按钮 -->
+                <uni-td v-if="showOperation" align="center">
+                    <view class="operation-btns">
+                        <template v-for="(action, index) in operationActions" :key="index">
+                            <button v-if="showAction(action, row)" class="btn" size="mini"
+                                :type="action.type || 'default'" @click="handleAction(action, row)">
+                                <cwg-icon :name="action.icon" :size="16" color="#fff" v-if="action.icon" />
+                                {{ action.text }}
+                            </button>
+                        </template>
+                    </view>
+                </uni-td>
+            </uni-tr>
+
+            <!-- 空数据提示 -->
+            <uni-tr v-if="tableData.length === 0">
+                <uni-td :colspan="columnSpan" align="center">
+                    <view class="empty-data">
+                        <uni-icons type="info" size="30" color="#999"></uni-icons>
+                        <text class="empty-text">暂无数据</text>
+                    </view>
+                </uni-td>
+            </uni-tr>
+        </uni-table>
+
+        <!-- 分页组件 -->
+        <view class="pagination-container" v-if="showPagination">
+            <view class="pagination-info">
+                <text>共 {{ pagination.total }} 条记录</text>
+                <view v-if="showPageSize" class="page-size-select">
+                    <text>每页显示</text>
+                    <picker @change="handlePageSizeChange" :value="pageSizeIndex" :range="pageSizes">
+                        <view class="page-size-value">{{ pagination.pageSize }}条/页</view>
+                    </picker>
+                </view>
+            </view>
+            <view class="pagination">
+                <view class="page-item prev" :class="{ disabled: pagination.current === 1 }"
+                    @click="handlePageChange('prev')">
+                    <uni-icons type="arrowleft" size="16" color="#666"></uni-icons>
+                    <text>上一页</text>
+                </view>
+
+                <view class="page-numbers">
+                    <view v-for="page in visiblePages" :key="page" class="page-number"
+                        :class="{ active: pagination.current === page }" @click="handlePageChange(page)">
+                        {{ page }}
+                    </view>
+                </view>
+
+                <view class="page-item next" :class="{ disabled: pagination.current === pagination.pages }"
+                    @click="handlePageChange('next')">
+                    <text>下一页</text>
+                    <uni-icons type="arrowright" size="16" color="#666"></uni-icons>
+                </view>
+            </view>
+        </view>
+    </view>
+</template>
+
+<script setup>
+import { ref, computed, watch, onMounted } from 'vue'
+
+const props = defineProps({
+    // 表格列配置
+    columns: {
+        type: Array,
+        required: true,
+        default: () => []
+    },
+    // API 请求函数
+    api: {
+        type: Function,
+        required: true
+    },
+    // 查询参数
+    queryParams: {
+        type: Object,
+        default: () => ({})
+    },
+    // 是否立即加载
+    immediate: {
+        type: Boolean,
+        default: true
+    },
+    // 选择类型:'selection' | null
+    selectionType: {
+        type: String,
+        default: null
+    },
+    // 是否显示操作列
+    showOperation: {
+        type: Boolean,
+        default: false
+    },
+    // 操作按钮配置
+    operationActions: {
+        type: Array,
+        default: () => [
+            { text: '编辑', type: 'primary', action: 'edit', icon: 'crm-puls' },
+            { text: '删除', type: 'warn', action: 'delete', icon: 'crm-delete' },
+            { text: '查看', type: 'default', action: 'view', icon: 'view' }
+        ]
+    },
+    // 是否显示底部操作栏
+    showBottomActions: {
+        type: Boolean,
+        default: false
+    },
+    // 底部操作按钮
+    bottomActions: {
+        type: Array,
+        default: () => []
+    },
+    // 是否显示每页条数选择
+    showPageSize: {
+        type: Boolean,
+        default: true
+    },
+    // 是否显示分页
+    showPagination: {
+        type: Boolean,
+        default: true
+    },
+    // 每页条数选项
+    pageSizes: {
+        type: Array,
+        default: () => [10, 20, 30, 50, 100]
+    },
+    // 默认每页条数
+    defaultPageSize: {
+        type: Number,
+        default: 10
+    }
+})
+
+const emit = defineEmits([
+    'selection-change',
+    'action-click',
+    'bottom-action-click',
+    'page-change',
+    'load-success',
+    'load-error'
+])
+
+// 表格数据
+const tableData = ref([])
+const selectedItems = ref([])
+
+// 分页参数
+const pagination = ref({
+    current: 1,
+    pageSize: props.defaultPageSize,
+    total: 0,
+    pages: 0
+})
+
+// 加载状态
+const loading = ref(false)
+
+// 计算列跨度
+const columnSpan = computed(() => {
+    let span = props.columns.length
+    if (props.showOperation) span += 1
+    return span
+})
+
+// 计算每页条数索引
+const pageSizeIndex = computed(() => {
+    return props.pageSizes.indexOf(pagination.value.pageSize)
+})
+
+// 计算显示的页码
+const visiblePages = computed(() => {
+    const maxVisible = 5
+    const half = Math.floor(maxVisible / 2)
+    let start = Math.max(1, pagination.value.current - half)
+    let end = Math.min(pagination.value.pages, start + maxVisible - 1)
+
+    if (end - start + 1 < maxVisible) {
+        start = Math.max(1, end - maxVisible + 1)
+    }
+
+    return Array.from({ length: end - start + 1 }, (_, i) => start + i)
+})
+
+// 监听查询参数变化
+watch(() => props.queryParams, () => {
+    console.log(props.queryParams, 123123);
+
+    refreshTable()
+}, { deep: true })
+
+// 加载数据
+const loadData = async () => {
+    if (loading.value) return
+
+    loading.value = true
+    try {
+        const params = {
+            page: {
+                current: pagination.value.current,
+                size: pagination.value.pageSize
+            },
+            ...props.queryParams
+        }
+
+        const res = await props.api(params)
+
+        // 处理不同的API返回格式
+        if (res.code === 200 || res.code === 0) {
+            const data = res.data || res
+
+            // 支持不同的分页数据结构
+            if (Array.isArray(data)) {
+                tableData.value = data
+                pagination.value.total = data.length
+                pagination.value.pages = 1
+            } else if (data.list || data.records) {
+                tableData.value = data.list || data.records
+                pagination.value.total = data.total || data.list?.length || 0
+                pagination.value.pages = data.pages || Math.ceil(pagination.value.total / pagination.value.pageSize)
+            } else {
+                tableData.value = []
+            }
+
+            emit('load-success', res)
+        } else {
+            throw new Error(res.message || '加载失败')
+        }
+    } catch (error) {
+        console.error('表格数据加载失败:', error)
+        uni.showToast({
+            title: error.message || '加载失败',
+            icon: 'none'
+        })
+        emit('load-error', error)
+    } finally {
+        loading.value = false
+    }
+}
+
+// 刷新表格
+const refreshTable = () => {
+    pagination.value.current = 1
+    loadData()
+}
+
+// 重新加载
+const reload = () => {
+    loadData()
+}
+
+// 分页变化
+const handlePageChange = (page) => {
+    if (page === 'prev') {
+        if (pagination.value.current > 1) {
+            pagination.value.current--
+        } else {
+            return
+        }
+    } else if (page === 'next') {
+        if (pagination.value.current < pagination.value.pages) {
+            pagination.value.current++
+        } else {
+            return
+        }
+    } else {
+        pagination.value.current = page
+    }
+
+    loadData()
+    emit('page-change', pagination.value)
+}
+
+// 每页条数变化
+const handlePageSizeChange = (e) => {
+    const size = props.pageSizes[e.detail.value]
+    pagination.value.pageSize = size
+    pagination.value.current = 1
+    loadData()
+}
+
+// 选择变化
+const handleSelectionChange = (e) => {
+    selectedItems.value = e.detail.value
+    emit('selection-change', selectedItems.value)
+}
+
+// 操作按钮点击
+const handleAction = (action, row) => {
+    emit('action-click', { action: action.action, row, original: action })
+}
+
+// 底部操作点击
+const handleBottomAction = (action) => {
+    emit('bottom-action-click', action)
+}
+
+// 判断是否显示操作按钮
+const showAction = (action, row) => {
+    if (action.show) {
+        return action.show(row)
+    }
+    return true
+}
+
+// 格式化单元格值
+const formatCellValue = (value, column, row) => {
+    if (column.formatter) {
+        return column.formatter({ value, row })
+    }
+
+    if (column.type === 'date' && value) {
+        return formatDate(value, column.dateFormat || 'YYYY-MM-DD HH:mm:ss')
+    }
+
+    if (value === null || value === undefined) {
+        return '-'
+    }
+
+    return value
+}
+
+// 格式化标签文本
+const formatTagText = (value, column) => {
+    if (column.tagMap && column.tagMap[value]) {
+        return column.tagMap[value]
+    }
+    return value || '-'
+}
+
+// 格式化标签类型
+const formatTagType = (value, column) => {
+    if (column.tagTypeMap && column.tagTypeMap[value]) {
+        return column.tagTypeMap[value]
+    }
+    return 'default'
+}
+
+// 格式化日期
+const formatDate = (date, format) => {
+    if (!date) return '-'
+    const d = new Date(date)
+    const year = d.getFullYear()
+    const month = String(d.getMonth() + 1).padStart(2, '0')
+    const day = String(d.getDate()).padStart(2, '0')
+    const hour = String(d.getHours()).padStart(2, '0')
+    const minute = String(d.getMinutes()).padStart(2, '0')
+    const second = String(d.getSeconds()).padStart(2, '0')
+
+    return format
+        .replace('YYYY', year)
+        .replace('MM', month)
+        .replace('DD', day)
+        .replace('HH', hour)
+        .replace('mm', minute)
+        .replace('ss', second)
+}
+
+// 预览图片
+const handlePreviewImage = (url) => {
+    uni.previewImage({
+        urls: [url]
+    })
+}
+
+// 获取选中项
+const getSelectedItems = () => {
+    return selectedItems.value
+}
+
+// 清空选中
+const clearSelection = () => {
+    selectedItems.value = []
+}
+
+// 暴露方法给父组件
+defineExpose({
+    refreshTable,
+    reload,
+    getSelectedItems,
+    clearSelection,
+    loadData,
+    tableData,
+    pagination
+})
+
+// 初始化加载
+onMounted(() => {
+    if (props.immediate) {
+        loadData()
+    }
+})
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.table-container {
+    max-height: calc(100vh - 209px);
+    overflow: hidden;
+    color: var(--color-slate-800);
+
+    :deep(.uni-table-scroll) {
+        width: 100%;
+        max-height: calc(100vh - 375px);
+        overflow: auto;
+    }
+
+    :depp(.table--border) {
+        border: none !important;
+    }
+
+    :deep(.uni-table) {
+
+        /* 固定表头样式 */
+        .uni-table-th {
+            position: sticky;
+            top: 0;
+            z-index: 100;
+            background-color: var(--color-slate-200) !important;
+            padding: px2rpx(12);
+            color: var(--color-slate-800);
+            font-weight: 600;
+
+            &::after {
+                display: none;
+            }
+        }
+
+
+        .tr-table--border,
+        .table--border {
+            border: none !important;
+        }
+
+        .uni-table-tr {
+            border-bottom: 5px solid var(--color-slate-200) !important;
+        }
+
+        .uni-table-th,
+        .uni-table-td {
+            padding: px2rpx(12) px2rpx(20);
+            color: var(--color-slate-800);
+        }
+    }
+
+}
+
+.table-image {
+    width: px2rpx(30);
+    height: px2rpx(30);
+    border-radius: px2rpx(8);
+}
+
+.operation-btns {
+    display: flex;
+    gap: px2rpx(10);
+    justify-content: center;
+    flex-wrap: wrap;
+}
+
+.btn {
+    margin: 0 px2rpx(4);
+    font-size: px2rpx(16);
+    padding: 0 px2rpx(20);
+    height: px2rpx(36);
+    border-radius: none;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+}
+
+.empty-data {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: px2rpx(60) 0;
+}
+
+.empty-text {
+    margin-top: px2rpx(20);
+    font-size: px2rpx(16);
+    color: #999;
+}
+
+.bottom-actions {
+    margin: px2rpx(30) px2rpx(20);
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.left-actions,
+.right-actions {
+    display: flex;
+    gap: px2rpx(20);
+}
+
+/* 分页样式 */
+.pagination-container {
+    margin: px2rpx(20) px2rpx(20);
+    display: flex;
+    align-items: center;
+    justify-content: flex-end;
+    gap: px2rpx(20);
+
+    .pagination {
+        display: flex;
+        align-items: center;
+        padding: px2rpx(10);
+    }
+
+    .page-item {
+        display: flex;
+        align-items: center;
+        padding: 0 px2rpx(12);
+        height: px2rpx(30);
+        background-color: #fff;
+        border: 1px solid #dcdfe6;
+        border-radius: px2rpx(8);
+        font-size: px2rpx(16);
+        color: #606266;
+        cursor: pointer;
+        transition: all 0.3s;
+        margin: 0 px2rpx(6);
+    }
+
+    .page-item.prev {
+        margin-right: px2rpx(10);
+    }
+
+    .page-item.next {
+        margin-left: px2rpx(10);
+    }
+
+    .page-item:not(.disabled):hover {
+        color: #007aff;
+        border-color: #007aff;
+    }
+
+    .page-item.disabled {
+        opacity: 0.5;
+        cursor: not-allowed;
+        pointer-events: none;
+    }
+
+    .page-numbers {
+        display: flex;
+        gap: px2rpx(8);
+    }
+
+    .page-number {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        min-width: px2rpx(40);
+        height: px2rpx(30);
+        padding: 0 px2rpx(4);
+        background-color: #fff;
+        border-radius: px2rpx(8);
+        font-size: px2rpx(16);
+        color: #606266;
+        cursor: pointer;
+        transition: all 0.3s;
+    }
+
+    .page-number:hover {
+        color: #007aff;
+        border-color: #007aff;
+    }
+
+    .page-number.active {
+        background-color: #007aff;
+        border-color: #007aff;
+        color: #fff;
+    }
+
+    .pagination-info {
+        display: flex;
+        align-items: center;
+        gap: px2rpx(30);
+        font-size: px2rpx(16);
+        color: #909399;
+    }
+
+    .page-size-select {
+        display: flex;
+        align-items: center;
+        gap: px2rpx(10);
+    }
+
+    .page-size-value {
+        padding: px2rpx(6) px2rpx(20);
+        border: 1px solid #dcdfe6;
+        border-radius: px2rpx(8);
+        color: #606266;
+    }
+}
+
+
+
+
+
+
+
+
+@media screen and (max-width: 768px) {
+    .pagination {
+        flex-wrap: wrap;
+        justify-content: center;
+    }
+
+    .page-item {
+        padding: 0 px2rpx(16);
+    }
+
+    .page-number {
+        min-width: px2rpx(60);
+    }
+
+    .pagination-info {
+        flex-direction: column;
+        gap: px2rpx(10);
+    }
+}
+</style>

+ 88 - 0
components/cwg-video-player.vue

@@ -0,0 +1,88 @@
+<template>
+    <view class="video-section">
+        <view class="video-title">{{ t('PersonalManagement.KYCVerify.VideoTitle') }}</view>
+        <view class="video-wrapper" :style="wrapperStyle">
+            <iframe :src="videoUrl" class="demo-video" @load="onVideoLoad" @error="onVideoError" />
+        </view>
+    </view>
+</template>
+
+<script setup>
+import { computed, ref, watch } from 'vue'
+import { onLoad, onUnload } from '@dcloudio/uni-app'
+
+const props = defineProps({
+    videoUrl: {
+        type: String,
+        required: true
+    },
+    aspectRatio: {
+        type: Number,
+        default: 16 / 9 // 默认16:9比例
+    }
+})
+
+const wrapperStyle = computed(() => {
+    return {
+        width: '100%',
+        position: 'relative',
+        paddingBottom: `${(1 / props.aspectRatio) * 100}%`, // 根据宽高比计算padding
+        backgroundColor: '#000',
+        overflow: 'hidden'
+    }
+})
+
+const onVideoLoad = () => {
+    console.log('视频加载成功')
+}
+
+const onVideoError = (err) => {
+    console.error('视频加载失败', err)
+    uni.showToast({
+        title: '视频加载失败,请稍后重试',
+        icon: 'none'
+    })
+}
+</script>
+
+<style lang="scss" scoped>
+.video-section {
+    margin-top: 40rpx;
+
+    .video-title {
+        font-size: 28rpx;
+        font-weight: 600;
+        color: #333;
+        margin-bottom: 20rpx;
+        padding-left: 20rpx;
+    }
+
+    .video-wrapper {
+        position: relative;
+        width: 100%;
+        border-radius: 16rpx;
+        overflow: hidden;
+        background: #000;
+
+        .demo-video {
+            position: absolute;
+            top: 0;
+            left: 0;
+            width: 100%;
+            height: 100%;
+            border: none;
+            background: #000;
+        }
+
+        web-view {
+            position: absolute;
+            top: 0;
+            left: 0;
+            width: 100%;
+            height: 100%;
+            border: none;
+            background: #000;
+        }
+    }
+}
+</style>

+ 0 - 0
composables/statusMachine.ts


+ 1 - 0
index.html

@@ -9,6 +9,7 @@
         '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
         (coverSupport ? ', viewport-fit=cover' : '') + '" />')
     </script>
+    <script type="text/javascript" src="/static/js/jsvm_all.js"></script>
     <title></title>
     <!--preload-links-->
     <!--app-context-->

+ 40 - 2
locale/cn.json

@@ -653,7 +653,7 @@
   "State": {
     "ToBeProcessed": "待处理",
     "InTheProcessing": "处理中",
-    "ToCertified": "认证",
+    "ToCertified": "视频认证",
     "Certified": "已认证",
     "Completed": "已完成",
     "Unfinished": "未完成",
@@ -1138,7 +1138,45 @@
       "NewEmail": "新邮箱",
       "MailboxVerificationCode": "邮箱验证码",
       "Men": "男",
-      "Women": "女"
+      "Women": "女",
+      "CertificationPhoto": "认证照片"
+    },
+    "CardVerify": {
+      "Title": "证件认证",
+      "UploadTitle": "上传证件照",
+      "UploadTip": "支持上传多张证件照(最多10张)",
+      "EmailCodeTitle": "邮箱验证码",
+      "EmailCodePlaceholder": "请输入邮箱验证码",
+      "GettingCode": "获取中...",
+      "NoticeTitle": "手持证件自拍(人工审核,1-2个工作日处理)",
+      "NoticeItem1": "上传本人手持有效身份证原件自拍照。",
+      "NoticeItem2": "上传一个包含二维码的加密货币钱包收款地址照片截图(用于二次核对,确保钱包地址准确无误)。",
+      "NoticeItem3": "必须使用与开户时一致的身份证原件进行拍摄,不接受打印件、复印件或电子文件。",
+      "NoticeItem4": "请确保证件信息、钱包地址二维码及相关文字内容清晰可见、无遮挡、无反光。",
+      "NoticeItem5": "照片须为正常拍摄方向,不接受镜像、翻转或经过处理的图片。",
+      "NoticeItem6": "请确保证件、钱包地址及人脸完整出现在画面中,画面清晰、边缘不缺失。",
+      "NoticeItem7": "认证操作须由交易账户持有人本人完成,且所展示的钱包地址必须为本人所有。",
+      "VideoTitle": "演示视频",
+      "VideoNotSupported": "您的浏览器不支持视频播放。",
+      "MissingBankId": "缺少钱包ID",
+      "CodeSentSuccess": "验证码发送成功",
+      "GetCodeFailed": "获取验证码失败",
+      "UploadAtLeastOne": "请至少上传一张证件照",
+      "EmailCodeRequired": "请输入邮箱验证码",
+      "WaitUploadComplete": "请等待文件上传完成",
+      "SubmitSuccess": "提交成功",
+      "SubmitFailed": "提交失败"
+    },
+    "KYCVerify": {
+      "NoticeTitle": "KYC人脸识别(安全可靠,操作简单)",
+      "NoticeItem1": "请使用手机原相机或浏览器内置扫描器进行拍摄或扫描,不支持使用微信、QQ 等第三方应用。*推荐浏览器:手机自有浏览器/edge/火狐/猎豹 /谷歌浏览器",
+      "NoticeItem2": "请使用与开户时一致的身份证件进行验证。",
+      "NoticeItem3": "请确保证件信息完整清晰、无遮挡、无反光。",
+      "NoticeItem4": "仅接受原件拍摄,不接受打印件或复印件。",
+      "NoticeItem5": "请确保证件及人脸位于系统规定的取景框内,画面清晰。",
+      "NoticeItem6": "认证操作必须由交易账户持有人本人完成,且人脸需与身份证照片保持一致。",
+      "VideoTitle": "演示视频",
+      "VideoNotSupported": "您的浏览器不支持视频播放。"
     }
   },
   "Drawer": {

+ 1 - 1
main.js

@@ -45,7 +45,7 @@ export function createApp() {
   app.use(uviewPlus);
   app.use(pinia);
   app.use(i18n);
-  // routeInterceptor.install()
+  routeInterceptor.install()
   return {
     app,
   };

+ 2 - 0
package.json

@@ -14,10 +14,12 @@
   "dependencies": {
     "@tencentcloud/chat-uikit-uniapp": "^2.2.4",
     "@vueuse/core": "^14.0.0",
+    "canvas": "^3.2.1",
     "clipboard": "^2.0.11",
     "crypto-js": "^4.2.0",
     "dayjs": "^1.11.13",
     "lodash": "^4.17.21",
+    "pdfh5": "^3.0.0",
     "pinyin-pro": "^3.27.0",
     "qrcode": "^1.5.4",
     "svgo": "^4.0.0",

+ 13 - 99
pages.json

@@ -17,63 +17,6 @@
         "navigationStyle": "custom"
       }
     },
-    {
-      "path": "pages/card/index",
-      "style": {
-        "navigationBarTitleText": "",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/card/operations",
-      "style": {
-        "navigationBarTitleText": "",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/card/select",
-      "style": {
-        "navigationBarTitleText": "",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/card/apply",
-      "style": {
-        "navigationBarTitleText": "",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/recharge-record/list",
-      "style": {
-        "navigationBarTitleText": "",
-        "navigationStyle": "custom",
-        "backgroundColor": "#f5f5f5"
-      }
-    },
-    {
-      "path": "pages/recharge-record/detail",
-      "style": {
-        "navigationBarTitleText": "",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/apply-record/list",
-      "style": {
-        "navigationBarTitleText": "",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/apply-record/detail",
-      "style": {
-        "navigationBarTitleText": "",
-        "navigationStyle": "custom"
-      }
-    },
     {
       "path": "pages/mine/index",
       "style": {
@@ -152,106 +95,77 @@
       }
     },
     {
-      "path": "pages/wallet/index",
-      "style": {
-        "navigationBarTitleText": "",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/wallet/global-list",
-      "style": {
-        "navigationBarTitleText": "",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/wallet/global-detail",
-      "style": {
-        "navigationBarTitleText": "",
-        "navigationStyle": "custom"
-      }
-    },
-    {
-      "path": "pages/wallet/balance",
+      "path": "pages/login/index",
       "style": {
         "navigationBarTitleText": "",
         "navigationStyle": "custom"
       }
     },
     {
-      "path": "pages/wallet/global-order",
-      "style": {
-        "navigationBarTitleText": "",
-        "navigationStyle": "custom",
-        "softinputMode": "adjustResize"
-      }
-    },
-    {
-      "path": "pages/wallet/vaultody",
+      "path": "pages/login/regist",
       "style": {
         "navigationBarTitleText": "",
         "navigationStyle": "custom"
       }
     },
     {
-      "path": "pages/wallet/vaultody-list",
+      "path": "pages/login/reset",
       "style": {
         "navigationBarTitleText": "",
         "navigationStyle": "custom"
       }
     },
     {
-      "path": "pages/wallet/withdraw",
+      "path": "pages/login/forget",
       "style": {
         "navigationBarTitleText": "",
         "navigationStyle": "custom"
       }
     },
     {
-      "path": "pages/wallet/withdraw-list",
+      "path": "pages/customer/index",
       "style": {
         "navigationBarTitleText": "",
         "navigationStyle": "custom"
       }
     },
     {
-      "path": "pages/wallet/withdraw-detail",
+      "path": "pages/customer/activities",
       "style": {
         "navigationBarTitleText": "",
         "navigationStyle": "custom"
       }
     },
     {
-      "path": "pages/login/index",
+      "path": "pages/customer/payment-history",
       "style": {
         "navigationBarTitleText": "",
         "navigationStyle": "custom"
       }
     },
     {
-      "path": "pages/login/regist",
+      "path": "pages/customer/recording-history",
       "style": {
         "navigationBarTitleText": "",
         "navigationStyle": "custom"
       }
     },
     {
-      "path": "pages/login/reset",
+      "path": "pages/customer/create-account",
       "style": {
         "navigationBarTitleText": "",
         "navigationStyle": "custom"
       }
     },
-    {
-      "path": "pages/login/forget",
+    { 
+      "path": "pages/mine/notice",
       "style": {
         "navigationBarTitleText": "",
         "navigationStyle": "custom"
       }
     },
-    {
-      "path": "pages/create-account/index",
+    { 
+      "path": "pages/mine/new",
       "style": {
         "navigationBarTitleText": "",
         "navigationStyle": "custom"

+ 0 - 0
pages/customer/activities.vue


+ 85 - 0
pages/customer/components/OrderStatusMachineCell.vue

@@ -0,0 +1,85 @@
+<template>
+  <view class="order-status-cell">
+    <!-- 状态标签 -->
+    <view class="status-badge" :class="statusClass">
+      {{ statusText }}
+    </view>
+
+    <!-- 取消按钮 -->
+    <view v-if="showCancelBtn" class="cancel-btn">
+      <span class="state btn crm-cursor" @click="handleCancel">
+        {{ t('Btn.Cancel') }}
+      </span>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import { createOrderStatusMachine } from '../composables/statusMachine'
+import { useI18n } from 'vue-i18n';
+const { t } = useI18n();
+const orderStatusMap = computed(() => ({
+  1: t('State.ToBeProcessed'),
+  2: t('State.Completed'),
+  3: t('State.InTheProcessing'),
+  4: t('State.Refused'),
+  5: t('State.expireTime'),
+  6: t('State.Cancelled')
+}));
+const props = defineProps({
+  row: {
+    type: Object,
+    required: true
+  },
+  currentTime: {
+    type: Number,
+    default: () => Date.now()
+  }
+})
+
+const emit = defineEmits(['cancel', 'cancelBackstage'])
+
+// 创建状态机实例
+const machine = computed(() => {
+  return createOrderStatusMachine(props.row, props.currentTime)
+})
+
+// 状态信息
+const statusInfo = computed(() => {
+  return machine.value.getStatusInfo()
+})
+
+// 状态文本
+const statusText = computed(() => {
+  return orderStatusMap.value[statusInfo.value.value]
+})
+
+// 状态样式类
+const statusClass = computed(() => {
+  const value = statusInfo.value.value
+  const classMap = {
+    1: 'status-warning',      // 待处理 - 使用橙色/琥珀色
+    2: 'status-success',      // 已完成 - 使用绿色
+    3: 'status-processing',   // 进行中 - 使用蓝色/天蓝
+    4: 'status-danger',       // 已拒绝 - 使用红色/玫瑰色
+    5: 'status-expired',      // 已过期 - 使用石板灰
+    6: 'status-cancelled'     // 已取消 - 使用石板灰
+  }
+  return classMap[value] || ''
+})
+
+// 是否显示取消按钮
+const showCancelBtn = computed(() => {
+  return machine.value.canCancel() || machine.value.canCancelBackstage()
+})
+
+// 处理取消
+const handleCancel = () => {
+  if (machine.value.canCancelBackstage()) {
+    emit('cancelBackstage', props.row.id)
+  } else {
+    emit('cancel', props.row.id)
+  }
+}
+</script>

+ 260 - 0
pages/customer/composables/statusMachine.ts

@@ -0,0 +1,260 @@
+// statusMachine.ts - 完整修正版
+export class OrderStatusMachine {
+    constructor(row, currentTime) {
+        this.row = row
+        this.currentTime = currentTime || Date.now()
+    }
+
+    // 获取当前时间(带时区处理)
+    getCurrentTime() {
+        // 如果传入了currentTime就直接使用
+        if (this.currentTime) return this.currentTime
+        
+        let timezone = 2; // 目标时区时间,东3区
+        let offset_GMT = new Date().getTimezoneOffset(); // 本地时间和格林威治的时间差,单位为分钟
+        let nowDate = new Date().getTime(); // 本地时间距 1970 年 1 月 1 日午夜(GMT 时间)之间的毫秒数
+        let now = new Date(
+            nowDate + offset_GMT * 60 * 1000 + timezone * 60 * 60 * 1000
+        );
+        return now.getTime()
+    }
+
+    // 是否未过期(当前时间 < 过期时间)
+    isNotExpired() {
+        // 如果没有过期时间,视为未过期
+        if (!this.row.expireTime) return true
+        
+        const now = this.getCurrentTime()
+        const expireTime = new Date(this.row.expireTime).getTime()
+        
+        return now < expireTime
+    }
+
+    // 是否已过期(当前时间 > 过期时间,且不是最终状态)
+    isExpired() {
+        const r = this.row
+
+        // 如果没有过期时间,不过期
+        if (!r.expireTime) return false
+
+        const now = this.getCurrentTime()
+        const expireTime = new Date(r.expireTime).getTime()
+        
+        // 当前时间必须大于过期时间
+        if (now <= expireTime) return false
+
+        // 已取消的不过期
+        if (this.isCancelled()) return false
+
+        // 已拒绝的不过期
+        if (this.isRefused()) return false
+
+        // 已完成的不过期
+        if (r.type === 1 && this.isCompletedType1()) return false
+        if (r.type === 2 && this.isCompletedType2()) return false
+
+        // 状态5已经在isCancelled中处理了,这里不需要重复判断
+        
+        return true
+    }
+
+    // 判断是否已完成 (type 1)
+    isCompletedType1() {
+        return this.row.status === 2 &&
+            this.row.executionStatus === 2 &&
+            this.row.callbackStatus === 1
+    }
+
+    // 判断是否已完成 (type 2)
+    isCompletedType2() {
+        return this.row.status === 2 &&
+            this.row.executionStatus === 2 &&
+            this.row.submitStatus === 2
+    }
+
+    // 判断是否已拒绝
+    isRefused() {
+        return this.row.callbackStatus === 2 ||
+            this.row.status === 3 ||
+            this.row.executionStatus === 3
+    }
+
+    // 判断是否已取消
+    isCancelled() {
+        return this.row.status === 5
+    }
+
+    // 判断是否为待处理状态 (type 1)
+    isToBeProcessedType1() {
+        const r = this.row
+        return r.status === 1 &&
+            r.callbackStatus === 0 &&
+            (r.expireTime === null || this.isNotExpired())
+    }
+
+    // 判断是否为待处理状态 (type 2)
+    isToBeProcessedType2() {
+        const r = this.row
+        return r.status === 1 &&
+            r.callbackStatus === 0 &&
+            (r.expireTime === null || this.isNotExpired())
+    }
+
+    // 判断是否为进行中状态 (type 1)
+    isInProgressType1() {
+        const r = this.row
+        
+        // 条件1: status=2, executionStatus=0或1, callbackStatus≠2, 未过期
+        const condition1 = r.status === 2 &&
+            [0, 1].includes(r.executionStatus) &&
+            r.callbackStatus !== 2 &&
+            (r.expireTime === null || this.isNotExpired())
+        
+        // 条件2: status=1, callbackStatus=1, 未过期
+        const condition2 = r.status === 1 &&
+            r.callbackStatus === 1 &&
+            (r.expireTime === null || this.isNotExpired())
+        
+        return condition1 || condition2
+    }
+
+    // 判断是否为进行中状态 (type 2)
+    isInProgressType2() {
+        const r = this.row
+        
+        // 条件1: status=2, executionStatus=0或1, callbackStatus≠2
+        const condition1 = r.status === 2 &&
+            [0, 1].includes(r.executionStatus) &&
+            r.callbackStatus !== 2
+        
+        // 条件2: status=2, executionStatus=2, callbackStatus=0, submitStatus≠2
+        const condition2 = r.status === 2 &&
+            r.executionStatus === 2 &&
+            r.callbackStatus === 0 &&
+            r.submitStatus !== 2
+        
+        return condition1 || condition2
+    }
+
+    // 获取状态码
+    getStatus() {
+        const r = this.row
+
+        // 1. 优先检查已取消
+        if (this.isCancelled()) return 6
+
+        // 2. 检查已拒绝
+        if (this.isRefused()) return 4
+
+        // 3. 检查是否已完成
+        if (r.type === 1 && this.isCompletedType1()) return 2
+        if (r.type === 2 && this.isCompletedType2()) return 2
+
+        // 4. 检查是否过期 - 按照原始逻辑,只有不是已完成、已拒绝、已取消才判断过期
+        if (this.isExpired()) return 5
+
+        // 5. 根据类型判断待处理或进行中
+        if (r.type === 1) {
+            if (this.isToBeProcessedType1()) return 1
+            if (this.isInProgressType1()) return 3
+        } else if (r.type === 2) {
+            if (this.isToBeProcessedType2()) return 1
+            if (this.isInProgressType2()) return 3
+        }
+
+        return null
+    }
+
+    // 获取状态信息
+    getStatusInfo() {
+        const status = this.getStatus()
+        const statusMap = {
+            1: { text: '待处理', class: 'warning', value: 1 },
+            2: { text: '已完成', class: 'success', value: 2 },
+            3: { text: '进行中', class: 'processing', value: 3 },
+            4: { text: '已拒绝', class: 'danger', value: 4 },
+            5: { text: '已过期', class: 'expired', value: 5 },
+            6: { text: '已取消', class: 'cancelled', value: 6 }
+        }
+        return statusMap[status] || { text: '-', class: '', value: null }
+    }
+
+    // 检查是否可以取消(普通取消)
+    canCancel() {
+        const r = this.row
+
+        // type=2 且 status=1, callbackStatus=0, 未过期
+        if (r.type === 2 &&
+            r.status === 1 &&
+            r.callbackStatus === 0 &&
+            (r.expireTime === null || this.isNotExpired())) {
+            return 'normal'
+        }
+
+        return false
+    }
+
+    // 检查是否可以取消(后台取消)
+    canCancelBackstage() {
+        const r = this.row
+
+        // type=2 且 status=2, executionStatus=2, backstageStatus=1
+        if (r.type === 2 &&
+            r.status === 2 &&
+            r.executionStatus === 2 &&
+            r.backstageStatus === 1) {
+            return 'backstage'
+        }
+
+        return false
+    }
+
+    // 获取可执行的操作列表
+    getAvailableActions() {
+        const actions = []
+
+        if (this.canCancel()) {
+            actions.push({
+                type: 'cancel',
+                text: '取消',
+                handler: 'handleCancel'
+            })
+        }
+
+        if (this.canCancelBackstage()) {
+            actions.push({
+                type: 'cancelBackstage',
+                text: '取消',
+                handler: 'handleCancelBackstage'
+            })
+        }
+
+        return actions
+    }
+
+    // 调试方法
+    debug() {
+        console.log({
+            row: this.row,
+            isCancelled: this.isCancelled(),
+            isRefused: this.isRefused(),
+            isCompletedType1: this.isCompletedType1(),
+            isCompletedType2: this.isCompletedType2(),
+            isExpired: this.isExpired(),
+            isNotExpired: this.isNotExpired(),
+            isToBeProcessedType1: this.isToBeProcessedType1(),
+            isToBeProcessedType2: this.isToBeProcessedType2(),
+            isInProgressType1: this.isInProgressType1(),
+            isInProgressType2: this.isInProgressType2(),
+            canCancel: this.canCancel(),
+            canCancelBackstage: this.canCancelBackstage(),
+            finalStatus: this.getStatus(),
+            finalStatusInfo: this.getStatusInfo()
+        })
+    }
+}
+
+// 工厂函数
+export const createOrderStatusMachine = (row, currentTime) => {
+    return new OrderStatusMachine(row, currentTime)
+}

+ 395 - 0
pages/customer/create-account.vue

@@ -0,0 +1,395 @@
+<template>
+    <cwg-page-wrapper class="create-page" :isHeaderFixed="true">
+        <uni-card class="create-content" :margin="0" :spacing="0">
+            <view class="content-title">
+                Create A New Account
+            </view>
+            <uni-row class="demo-uni-row">
+                <cwg-match-media :min-width="991">
+                </cwg-match-media>
+                <uni-col :xs="24" :sm="24" :md="12" :lg="10" :xl="8" class="right-f">
+                    <navigator url="/pages/login/regist" class="account-tip">
+                        {{ t("signin.words") }}
+                        <text>{{ t("signin.signup") }}</text>
+                    </navigator>
+                    <cwg-match-media :min-width="791">
+
+                    </cwg-match-media>
+                </uni-col>
+            </uni-row>
+        </uni-card>
+
+    </cwg-page-wrapper>
+</template>
+<script setup>
+import { ref, onMounted, computed } from "vue";
+import QrCode from "@/components/QrCode.vue";
+import { post } from "@/utils/request";
+import { userToken } from "@/composables/config";
+import { userApi } from "@/api/user";
+import { ucardApi } from "@/api/ucard";
+import useUserStore from "@/stores/use-user-store";
+import useRouter from "@/hooks/useRouter";
+import { useI18n } from "vue-i18n";
+import logoImage from "/static/images/logo3.png";
+const router = useRouter();
+const { t } = useI18n();
+const userStore = useUserStore();
+// 响应式表单数据
+const form = ref({
+    loginName: "",
+    password: "",
+});
+
+function submit() {
+    if (!form.value.loginName) {
+        uni.$u.toast(t("signin.form.email"));
+        return;
+    }
+    if (!form.value.password) {
+        uni.$u.toast(t("signin.form.password"));
+        return;
+    }
+
+    handleLogin();
+}
+
+const fetchUserList = (params) => post("/Login/AcctLogin", params);
+async function handleLogin() {
+    try {
+        const res = await userApi.login({
+            loginName: form.value.loginName,
+            password: form.value.password,
+        });
+        if (res.code === 200) {
+            userToken.value = res.data;
+            uni.$u.toast(t("login.msg0_1"));
+            getUserInfo();
+            reasonsRefusalList();
+            if (remenber.value.length) {
+                userStore.saveAccountInfo({
+                    loginName: form.value.loginName,
+                    password: form.value.password,
+                    rememberPassword: true,
+                });
+            } else {
+                userStore.saveAccountInfo({
+                    loginName: "",
+                    password: "",
+                    rememberPassword: false,
+                });
+            }
+            //  console.log(1111);
+        } else {
+            //  console.log(12112);
+        }
+    } catch (error) {
+        //  console.log(error, 19089);
+    }
+}
+async function getUserInfo() {
+    try {
+        const res = await ucardApi.getSingle();
+        userStore.saveUserInfo(res.data);
+        if (res.code === 200) {
+            if (!res.data || res.data.approveStatus != 2) {
+                router.push("/pages/mine/improve");
+            } else {
+                router.push("/pages/card/index");
+            }
+        } else {
+            uni.$u.toast(res.msg || t("login.msg0"));
+        }
+    } catch (error) {
+        //  console.log(error, 111);
+    }
+}
+async function reasonsRefusalList() {
+    try {
+        const res = await ucardApi.reasonsRefusalList();
+        if (res.code === 200) {
+            pickFields(res.data);
+        } 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);
+}
+
+onMounted(() => {
+    const accountInfo = userStore.accountInfo;
+    if (accountInfo?.rememberPassword) {
+        form.value.loginName = accountInfo?.loginName || "";
+        form.value.password = accountInfo?.password || "";
+        remenber.value = ["记住我"];
+    } else {
+        form.value.loginName = "";
+        form.value.password = "";
+        remenber.value = [];
+    }
+});
+const inputType = ref("password");
+</script>
+
+
+<style lang="scss" scoped>
+@import "@/uni.scss";
+
+.create-page {
+    height: 100vh;
+    border: none;
+    padding: 0;
+}
+
+.create-content {
+    margin: 0;
+}
+
+.demo-uni-row {
+    margin: 0 !important;
+
+    .left-bg {
+        height: calc(100vh - 60px);
+        background-image: url(/static/images/login-bg.gif);
+        background-repeat: no-repeat;
+        background-size: cover;
+        background-position: center center;
+
+        .left-box {
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+
+            .h1 {
+                // text-align: center;
+                line-height: 20px;
+                color: #fff;
+                font-size: 30px;
+                margin-top: 30px;
+                font-size: 700;
+                line-height: 1.5;
+            }
+
+            .h6 {
+                text-align: start;
+                line-height: 20px;
+                color: #fff;
+                font-size: 14px;
+                margin-top: 10px;
+            }
+
+            .company {
+                padding: px2rpx(40) 0 px2rpx(50) 0;
+                position: relative;
+                align-items: flex-start !important;
+            }
+        }
+
+        .left-content {
+            .h1 {
+                // text-align: center;
+                line-height: 20px;
+                color: #fff;
+                font-size: 30px;
+                margin-top: 30px;
+                font-size: 700;
+                line-height: 1.5;
+            }
+
+            .h6 {
+                line-height: 20px;
+                color: #fff;
+                font-size: 14px;
+                margin-top: 10px;
+            }
+        }
+    }
+
+    .right-f {
+        background-color: #fff;
+        padding: 0 px2rpx(24);
+        box-sizing: border-box;
+
+        .account {
+            background-color: #ffffff;
+            position: relative;
+            height: calc(100vh - 60px);
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            padding: 0 10%;
+
+            .company {
+                padding: px2rpx(50) 0 px2rpx(20) 0;
+                position: relative;
+                align-items: center !important;
+            }
+
+            .company-icon {
+                width: px2rpx(234);
+            }
+        }
+    }
+}
+
+.bottom-box {
+    width: 100%;
+    height: 60px;
+    background-color: #fff;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    color: #000;
+
+    .bottom-title {
+
+        text-align: center;
+        font-size: px2rpx(14);
+        font-weight: 500;
+        line-height: 1.5;
+        color: #666666;
+
+    }
+
+    .ellipsis {
+        width: px2rpx(200);
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+    }
+
+    .cwg-button {
+        width: 120px !important;
+        padding: px2rpx(4) 0 !important;
+    }
+}
+
+
+
+
+button {
+    background-color: #ea002a;
+    font-size: px2rpx(14);
+    font-weight: normal;
+    height: px2rpx(44);
+    line-height: px2rpx(44);
+}
+
+.company {
+    padding: px2rpx(50) 0 px2rpx(200) 0;
+    position: relative;
+    align-items: flex-start !important;
+}
+
+.logo {
+    margin-left: px2rpx(48);
+}
+
+.title {
+    margin: px2rpx(32) 0;
+    font-size: px2rpx(24);
+    font-weight: bolder;
+    color: #e4e4e4;
+    text-align: center;
+
+    i {
+        margin-right: px2rpx(10);
+    }
+
+    .tit1 {
+        font-size: px2rpx(34);
+        line-height: 1.5;
+        font-weight: bold;
+        color: #000000;
+    }
+
+    .tit2 {
+        font-size: px2rpx(16);
+        line-height: 1.5;
+        color: #cecece;
+        font-weight: 500;
+    }
+}
+
+.qr-title {
+    font-size: px2rpx(16);
+    line-height: 1.5;
+    color: #cecece;
+    font-weight: 500;
+    text-align: center;
+    margin: px2rpx(40) 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .line {
+        flex: 1;
+        height: 1px;
+        background-color: #e4e4e4;
+    }
+
+    .qr-tit2 {
+        margin: 0 px2rpx(12);
+
+    }
+
+}
+
+.input {
+    height: px2rpx(44);
+    border-radius: px2rpx(8);
+    background: #f7f8fa;
+    padding: 0 px2rpx(20) !important;
+    position: relative;
+}
+
+.account-icon {
+    width: px2rpx(12);
+    height: px2rpx(14) !important;
+    margin-right: px2rpx(5);
+}
+
+:deep(.u-input__content__prefix-icon) {
+    height: px2rpx(20);
+}
+
+.regiset-btn {
+    margin: px2rpx(20) 0;
+}
+
+.account-tip {
+    color: #666666;
+    font-size: px2rpx(14);
+    text-align: center;
+
+    text {
+        color: #ea002a;
+    }
+}
+
+:deep(.u-form-item__body) {
+    padding: 0 !important;
+    padding-bottom: px2rpx(24) !important;
+}
+
+:deep(.wcg-checkbox) {
+    padding: 0 !important;
+}
+
+.cwg-button {
+    padding: px2rpx(34) 0 !important;
+}
+</style>

+ 0 - 0
pages/customer/index.vue


+ 200 - 0
pages/customer/payment-history.vue

@@ -0,0 +1,200 @@
+<template>
+    <cwg-page-wrapper class="create-page" :isHeaderFixed="true">
+        <view class="info-card">
+            <view class="content-title">
+                <view v-t="'Home.page_customer.item4'"></view>
+            </view>
+            <view class="search-bar">
+                <cwg-combox v-model:value="search.type" :options="typeMap" :placeholder="t('placeholder.choose')" />
+                <cwg-combox v-model:value="search.orderStatus" :options="orderStatusMap"
+                    :placeholder="t('Custom.PaymentHistory.StatusPlaceholder')" />
+                <uni-easyinput v-model="search.login" :placeholder="t('placeholder.login')" />
+                <uni-datetime-picker type="daterange" v-model="search.date"
+                    :placeholder="t('placeholder.Start') + ' - ' + t('placeholder.End')" />
+            </view>
+            <cwg-tabel ref="tableRef" :columns="columns" :queryParams="search" :api="listApi" :show-operation="false"
+                :showPagination="false">
+                <template #avatar="{ row }">
+                    <image :src="row.avatar" class="avatar" mode="widthFix" />
+                    <cwg-file :path="row.path" />
+                </template>
+                <template #type="{ row }">
+                    <view class="status-badge">{{typeMap.find(item => item.value === row.type)?.text}}
+                    </view>
+                </template>
+                <template #status="{ row }">
+                    <OrderStatusMachineCell :row="row" @cancel="handleOrderCancel" @action="handleOrderAction" />
+                </template>
+                <template #btn="{ row }">
+                    <text :class="['operation-btn', row.status !== 4 ? 'disabled' : '']" @click="openAddFile(row)">
+                        <cwg-icon name="crm-image" :size="16" color="#1d293d" />
+                        <text v-t="'State.Again'" />
+                    </text>
+                </template>
+            </cwg-tabel>
+        </view>
+    </cwg-page-wrapper>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, onMounted } from 'vue';
+import { useI18n } from 'vue-i18n';
+const { t, locale } = useI18n();
+import { financialApi } from '@/service/financial';
+import OrderStatusMachineCell from './components/OrderStatusMachineCell.vue'
+const search = ref({})
+const typeMap = computed(() => ([
+    { value: null, text: t('Custom.PaymentHistory.All') },
+    { value: 1, text: t('Custom.PaymentHistory.Deposit') },
+    { value: 2, text: t('Custom.PaymentHistory.Withdrawals') }
+]));
+const orderStatusMap = computed(() => ([
+    { value: null, text: t('Custom.PaymentHistory.All') },
+    { value: 1, text: t('State.ToBeProcessed') },
+    { value: 2, text: t('State.Completed') },
+    { value: 3, text: t('State.InTheProcessing') },
+    { value: 4, text: t('State.Refused') },
+    { value: 5, text: t('State.expireTime') },
+    { value: 6, text: t('State.Cancelled') },
+]));
+
+const handleOrderCancel = (row) => {
+    console.log('取消订单:', row)
+    // 处理取消逻辑
+}
+
+const isZh = computed(() => ['cn', 'zh', 'zhHant'].includes(locale.value));
+// 表格列配置
+const columns = ref([
+    {
+        prop: 'serial',
+        label: t('Custom.PaymentHistory.Serial'),
+        align: 'left'
+    },
+    {
+        prop: 'login',
+        label: t('Custom.PaymentHistory.TradingAccount'),
+        align: 'left'
+    },
+    {
+        prop: 'type',
+        label: t('Custom.PaymentHistory.payType'),
+        align: 'left',
+        slot: 'type'
+    },
+    {
+        prop: 'channelName',
+        label: t('Custom.PaymentHistory.PaymentMethod'),
+        formatter: ({ row }) => isZh.value ? row.channelName : row.channelEnName,
+        align: 'left'
+    },
+    {
+        prop: 'amount',
+        label: t('Custom.PaymentHistory.Amount'),
+        formatter: ({ row }) => row.amount + ' ' + row.currency,
+        align: 'left'
+    },
+    {
+        prop: 'addTime',
+        label: t('Custom.PaymentHistory.ApplicationDate'),
+        type: 'date',
+        dateFormat: 'YYYY-MM-DD HH:mm',
+        align: 'left'
+    },
+    {
+        prop: 'status',
+        label: t('Custom.PaymentHistory.Status'),
+        slot: 'status',
+        align: 'left'
+    },
+    {
+        prop: 'note',
+        label: t('Custom.Recording.Note'),
+        formatter: ({ row }) => row.note || '--',
+        align: 'left'
+    }
+])
+
+const addFileDialog = ref(null);
+const listApi = ref(null)
+listApi.value = financialApi.BalanceList
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.avatar {
+    width: px2rpx(60);
+    height: px2rpx(60);
+    border-radius: 4px;
+}
+
+.content-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-size: px2rpx(20);
+    font-weight: 500;
+
+    .content-title-btns {
+        margin: px2rpx(8) 0;
+
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: px2rpx(12);
+
+        .btn-primary {
+            min-width: px2rpx(120);
+            background-color: var(--color-error);
+            color: white;
+            padding: 0 px2rpx(12);
+            border: none;
+            font-size: px2rpx(14);
+            text-align: center;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            gap: px2rpx(8);
+        }
+
+        .btn-primary:active {
+            background-color: var(--color-navy-700);
+        }
+    }
+}
+
+.operation-btn {
+    :deep(span) {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: px2rpx(4);
+        cursor: pointer;
+        background-color: var(--color-slate-150);
+        padding: px2rpx(8) 0;
+    }
+}
+
+.operation-btn.disabled {
+    cursor: not-allowed;
+    opacity: 0.5;
+}
+
+.search-bar {
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+    flex-wrap: wrap;
+    gap: px2rpx(16);
+    margin: px2rpx(16) 0;
+
+    .cwg-combox,
+    .uni-easyinput,
+    .uni-date {
+        width: px2rpx(240) !important;
+        flex: none;
+    }
+}
+</style>

+ 0 - 0
pages/customer/recording-history.vue


+ 1 - 1
pages/launch/index.vue

@@ -8,7 +8,7 @@ onLoad(() => {
         })
     } else {
         uni.reLaunch({
-            url: '/pages/card/index'
+            url: '/pages/mine/index'
         })
     }
 })

+ 363 - 0
pages/mine/components/AddBankDialog.vue

@@ -0,0 +1,363 @@
+<template>
+    <uni-popup ref="popupRef" type="center" background-color="#fff">
+        <view class="dialog-container">
+            <view class="dialog-header">
+                <text class="dialog-title">{{ t('blockchain.item2') }}</text>
+                <view class="dialog-close" @click="close">
+                    <text>×</text>
+                </view>
+            </view>
+            <uni-forms ref="formRef" :rules="rules" :model="form" labelWidth="200" label-position="top"
+                class="crm-form">
+                <uni-row class="form-row uni-row1">
+                    <template v-if="form.type === 1">
+                        <uni-col :xs="24">
+                            <uni-forms-item>
+                                <cwg-file-picker v-model="form.bankFront" :editable="editingId === form.id" :limit="1"
+                                    uploadUrl="/custom/bank/upload" :baseUrl="updateUrl" :imageWidth="150"
+                                    :imageHeight="150" uploadText="点击上传" replaceText="点击替换" noImageText="暂无图片"
+                                    :showPreviewDelete="editingId === form.id"
+                                    @update:modelValue="(val) => handleFileUpdate(val, form, 'bankFront')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.BankAccountName')">
+                                <uni-easyinput :clearable="false" v-model="form.bankUname"
+                                    :placeholder="locale == 'es' ? 'Introduzca el nombre de la red' : t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.BankName')">
+                                <cwg-combox :clearable="false" :filterable="true" v-model:value="form.bankName"
+                                    :options="bankOptions" :placeholder="t('placeholder.choose')"
+                                    @change="onStateChange" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.BankAccount')">
+                                <uni-easyinput :clearable="false" v-model="form.bankCardNum"
+                                    :placeholder="locale == 'es' ? 'Introduzca la dirección de la billetera' : t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.AccountOpeningBranch')">
+                                <uni-easyinput :clearable="false" v-model="form.bankBranchName"
+                                    :placeholder="locale == 'es' ? 'Introduzca la dirección de la billetera' : t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                    </template>
+                    <template v-if="form.type === 4">
+                        <!-- 区块链名称 -->
+                        <uni-col :xs="24">
+                            <uni-forms-item prop="addressName" :label="t('blockchain.item3')">
+                                <uni-easyinput :clearable="false" v-model="form.addressName"
+                                    :placeholder="t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <!-- 钱包地址 -->
+                        <uni-col :xs="24">
+                            <uni-forms-item prop="address" :label="t('blockchain.item4')">
+                                <uni-easyinput :clearable="false" v-model="form.address"
+                                    :placeholder="t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                    </template>
+                    <template v-if="form.type === 2">
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.BankAccountName')">
+                                <uni-easyinput :clearable="false" v-model="form.bankUname" :disabled="true"
+                                    :placeholder="locale == 'es' ? 'Introduzca el nombre de la red' : t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.BankAccount')">
+                                <uni-easyinput :clearable="false" v-model="form.bankCardNum"
+                                    :placeholder="locale == 'es' ? 'Introduzca la dirección de la billetera' : t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.BankName')">
+                                <uni-easyinput :clearable="false" v-model="form.bankName"
+                                    :placeholder="locale == 'es' ? 'Introduzca el nombre del banco' : t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.BankAddress')">
+                                <uni-easyinput :clearable="false" v-model="form.bankAddr"
+                                    :placeholder="locale == 'es' ? 'Introduzca la dirección del banco' : t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.SwiftBIC')">
+                                <uni-easyinput :clearable="false" v-model="form.swiftCode"
+                                    :placeholder="locale == 'es' ? 'Introduzca el SWIFT/BIC' : t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.BankCode')">
+                                <uni-easyinput :clearable="false" v-model="form.bankCode"
+                                    :placeholder="locale == 'es' ? 'Introduzca el código del banco' : t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <uni-forms-item
+                                :label="locale == 'es' ? 'Número de sucursal (opcional)' : 'Account Agency NO'">
+                                <uni-easyinput :clearable="false" v-model="form.agencyNo"
+                                    :placeholder="locale == 'es' ? 'Introduzca el número de sucursal' : t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                    </template>
+                    <template v-if="form.type === 3">
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.CreditCardAccountName')">
+                                <uni-easyinput :clearable="false" v-model="form.bankUname" :disabled="true"
+                                    :placeholder="t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.CreditCardAccount')">
+                                <uni-easyinput :clearable="false" v-model="form.bankCardNum"
+                                    :placeholder="locale == 'es' ? 'Introduzca el número de tarjeta' : t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.ExpirationYear')">
+                                <uni-easyinput :clearable="false" v-model="form.expiryYearMonth"
+                                    :placeholder="locale == 'es' ? 'Introduzca MM/AA' : t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('CVV')">
+                                <uni-easyinput :clearable="false" v-model="form.cvv"
+                                    :placeholder="locale == 'es' ? 'Introduzca el CVV' : t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                    </template>
+                    <uni-col :xs="24">
+                        <uni-forms-item class="checkbox-item" prop="defaultBank">
+                            <uni-data-checkbox v-model="form.defaultBank" multiple :localdata="hobbys" />
+                        </uni-forms-item>
+                    </uni-col>
+                </uni-row>
+            </uni-forms>
+            <view class="dialog-footer">
+                <view class="btn btn-cancel" @click="close">{{ t('Btn.Cancel') }}</view>
+                <view class="btn btn-confirm" @click="submit">{{ t('Btn.Confirm') }}</view>
+            </view>
+        </view>
+    </uni-popup>
+</template>
+
+<script setup lang="ts">
+import { ref, nextTick, computed, onMounted } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { Validators } from '@/utils/validators';
+import { personalApi } from '@/service/personal';
+const { t, locale } = useI18n();
+
+interface AddBankForm {
+    addressName: string;
+    address: string;
+    checkboxGroup: string[];
+}
+const hobbys = computed(() => [
+    { value: 1, text: t('blockchain.item8') }
+]);
+const emit = defineEmits(["success"]);
+
+const popupRef = ref<any>(null);
+const formRef = ref<any>(null);
+
+const form = ref<AddBankForm>({});
+
+const rules = {
+    addressName: [Validators.required(t('blockchain.item3') + t('common.cannotbeempty'))],
+    address: [Validators.required(t('blockchain.item4') + t('common.cannotbeempty'))]
+};
+
+// 打开弹窗
+const open = async (type: number) => {
+    form.value = {}
+    await nextTick();
+    form.value.type = type;
+    getBankList()
+    popupRef.value?.open();
+};
+
+// 关闭弹窗
+const close = () => {
+    popupRef.value?.close();
+    resetForm();
+};
+
+// 重置表单
+const resetForm = () => {
+    form.value = {};
+    formRef.value?.clearValidate();
+};
+
+// 提交表单
+const submit = async () => {
+    console.log(form.value, 42342342);
+
+    try {
+        // 校验表单
+        const valid = await formRef.value?.validate();
+        console.log(valid, 23123);
+
+        if (!valid) {
+            return;
+        }
+        // 调用 API 添加钱包
+        const submitData = {
+            ...form.value,
+            expiryYear: form.value?.expiryYearMonth ? form.value.expiryYearMonth.split("/")[0] : undefined,
+            expiryMonth: form.value?.expiryYearMonth ? form.value.expiryYearMonth.split("/")[1] : undefined,
+            defaultBank: form.value?.defaultBank && form.value?.defaultBank[0] ? 1 : 0,
+        };
+        let res = await personalApi.customBankAdd({
+            bankUname: 'username',
+            ...submitData,
+        });
+        if (res.code == 200) {
+            uni.showToast({ title: t('Msg.Success'), icon: 'success' });
+            emit("success", res.data);
+        } else {
+            uni.showToast({ title: res.msg || t('common.error'), icon: 'none' });
+        }
+        close();
+
+    } catch (error: any) {
+        console.log(error);
+
+        uni.showToast({ title: error.message || error.msg || t('common.error'), icon: 'none' });
+    }
+};
+
+const bankList = ref([])
+const isZh = computed(() => ['cn', 'zh', 'zhHant'].includes(locale.value));
+
+const getLangName = (item: any) => (isZh.value ? item.name : item.enName);
+const createOptions = (list: any[], valueKey = 'code') => {
+    return list.map((item) => ({
+        text: getLangName(item),
+        value: getLangName(item)
+    }));
+};
+
+const bankOptions = computed(() => createOptions(bankList.value, 'name'));
+// 获取银行列表
+const getBankList = async () => {
+    const res = await personalApi.BankList({})
+    if (res.code === 200) {
+        bankList.value = res.data
+    }
+}
+// 文件更新处理
+const handleFileUpdate = (newValue, item, field) => {
+    item[field] = newValue
+}
+// 暴露方法
+defineExpose({
+    open,
+    close
+});
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.dialog-container {
+    width: 80vw;
+    max-width: px2rpx(800);
+    max-height: 85vh;
+    padding: px2rpx(24);
+    overflow: hidden;
+    border-radius: px2rpx(12);
+
+    .dialog-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: px2rpx(24);
+        padding-bottom: px2rpx(16);
+        border-bottom: 1px solid #f3f4f6;
+
+        .dialog-title {
+            font-size: px2rpx(18);
+            font-weight: 600;
+            color: #1f2937;
+        }
+
+        .dialog-close {
+            width: px2rpx(32);
+            height: px2rpx(32);
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: px2rpx(28);
+            color: #9ca3af;
+            cursor: pointer;
+            transition: all 0.3s;
+
+            &:hover {
+                color: #1f2937;
+            }
+        }
+    }
+
+    .dialog-footer {
+        display: flex;
+        gap: px2rpx(12);
+        justify-content: flex-end;
+        padding-top: px2rpx(16);
+        border-top: 1px solid #f3f4f6;
+
+        .btn {
+            min-width: px2rpx(120);
+            padding: px2rpx(12) px2rpx(24);
+            border-radius: px2rpx(6);
+            font-size: px2rpx(14);
+            font-weight: 600;
+            border: none;
+            cursor: pointer;
+            text-align: center;
+            transition: all 0.3s;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+
+            &.btn-cancel {
+                background: #f3f4f6;
+                color: #6b7280;
+
+                &:hover {
+                    background: #e5e7eb;
+                }
+
+                &:active {
+                    background: #d1d5db;
+                }
+            }
+
+            &.btn-confirm {
+                background: #ea2027;
+                color: #fff;
+
+                &:hover {
+                    background: #d11920;
+                }
+
+                &:active {
+                    background: #c01819;
+                }
+            }
+        }
+    }
+
+    .crm-form {
+        overflow-y: auto;
+        max-height: 70vh;
+    }
+}
+</style>

+ 261 - 0
pages/mine/components/AddFileDialog.vue

@@ -0,0 +1,261 @@
+<template>
+    <uni-popup ref="fileRef" type="center" background-color="#fff">
+        <view class="dialog-container">
+            <view class="dialog-header">
+                <text class="dialog-title">{{ form.title }}</text>
+                <view class="dialog-close" @click="close">
+                    <text>×</text>
+                </view>
+            </view>
+            <uni-forms ref="formRef" :rules="rules" :model="form" labelWidth="200" label-position="top"
+                class="crm-form">
+                <uni-row class="form-row uni-row1">
+                    <template v-if="form.type === 1">
+                        <uni-col :xs="24">
+
+                            <uni-forms-item :label="t('PersonalManagement.Title.ProofOfIdentity') + 1">
+                                <cwg-file-picker v-model="form.file1" :editable="editingId === form.id" :limit="1"
+                                    uploadUrl="/custom/file/upload/1" :baseUrl="updateUrl" :imageWidth="150"
+                                    :imageHeight="150" uploadText="点击上传" replaceText="点击替换" noImageText="暂无图片"
+                                    :showPreviewDelete="editingId === form.id"
+                                    @update:modelValue="(val) => handleFileUpdate(val, form, 'file1')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Title.ProofOfIdentity') + 2">
+                                <cwg-file-picker v-model="form.file2" :editable="editingId === form.id" :limit="1"
+                                    uploadUrl="/custom/file/upload/2" :baseUrl="updateUrl" :imageWidth="150"
+                                    :imageHeight="150" uploadText="点击上传" replaceText="点击替换" noImageText="暂无图片"
+                                    :showPreviewDelete="editingId === form.id"
+                                    @update:modelValue="(val) => handleFileUpdate(val, form, 'file2')" />
+                            </uni-forms-item>
+                        </uni-col>
+                    </template>
+                    <template v-if="form.type === 2">
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Title.ProofOfAddress')">
+                                <cwg-file-picker v-model="form.file3" :editable="editingId === form.id" :limit="1"
+                                    uploadUrl="/custom/file/upload/3" :baseUrl="updateUrl" :imageWidth="150"
+                                    :imageHeight="150" uploadText="点击上传" replaceText="点击替换" noImageText="暂无图片"
+                                    :showPreviewDelete="editingId === form.id"
+                                    @update:modelValue="(val) => handleFileUpdate(val, form, 'file3')" />
+                            </uni-forms-item>
+                        </uni-col>
+                    </template>
+                    <template v-if="form.type === 3">
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Title.AttachedFile')">
+                                <cwg-file-picker v-model="form.file4" :editable="editingId === form.id" :limit="9"
+                                    uploadUrl="/custom/file/upload/10" :baseUrl="updateUrl" :imageWidth="150"
+                                    :imageHeight="150" uploadText="点击上传" replaceText="点击替换" noImageText="暂无图片"
+                                    :showPreviewDelete="editingId === form.id"
+                                    @update:modelValue="(val) => handleFileUpdate(val, form, 'file4')" />
+                            </uni-forms-item>
+                        </uni-col>
+                    </template>
+                </uni-row>
+            </uni-forms>
+            <view class="dialog-footer">
+                <view class="btn btn-cancel" @click="close">{{ t('Btn.Cancel') }}</view>
+                <view class="btn btn-confirm" @click="submit">{{ t('Btn.Confirm') }}</view>
+            </view>
+        </view>
+    </uni-popup>
+</template>
+
+<script setup lang="ts">
+import { ref, nextTick, computed, onMounted } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { Validators } from '@/utils/validators';
+import { personalApi } from '@/service/personal';
+const { t, locale } = useI18n();
+
+interface AddBankForm {
+    addressName: string;
+    address: string;
+    checkboxGroup: string[];
+}
+const hobbys = computed(() => [
+    { value: 1, text: t('blockchain.item8') }
+]);
+const emit = defineEmits(["success"]);
+
+const fileRef = ref<any>(null);
+const formRef = ref<any>(null);
+
+const form = ref<AddBankForm>({});
+
+const rules = {
+    addressName: [Validators.required(t('blockchain.item3') + t('common.cannotbeempty'))],
+    address: [Validators.required(t('blockchain.item4') + t('common.cannotbeempty'))]
+};
+
+// 打开弹窗
+const open = async (item) => {
+    form.value = {}
+    await nextTick();
+    console.log(item, 23123);
+    form.value = item;
+    let files = []
+    item?.tableData.forEach(file => {
+        if (file.type === 1) {
+            form.value.file1 = file.path
+        } else if (file.type === 2) {
+            form.value.file2 = file.path
+        } else if (file.type === 3) {
+            form.value.file3 = file.path
+        } else if (file.type === 10) {
+            files.push(file.path)
+        }
+    })
+
+
+    getBankList()
+    fileRef.value?.open();
+};
+
+// 关闭弹窗
+const close = () => {
+    fileRef.value?.close();
+    resetForm();
+};
+
+// 重置表单
+const resetForm = () => {
+    form.value = {};
+    formRef.value?.clearValidate();
+};
+
+// 提交表单
+const submit = async () => {
+    close();
+    emit('success');
+};
+
+const bankList = ref([])
+const isZh = computed(() => ['cn', 'zh', 'zhHant'].includes(locale.value));
+
+const getLangName = (item: any) => (isZh.value ? item.name : item.enName);
+const createOptions = (list: any[], valueKey = 'code') => {
+    return list.map((item) => ({
+        text: getLangName(item),
+        value: getLangName(item)
+    }));
+};
+
+const bankOptions = computed(() => createOptions(bankList.value, 'name'));
+// 获取银行列表
+const getBankList = async () => {
+    const res = await personalApi.BankList({})
+    if (res.code === 200) {
+        bankList.value = res.data
+    }
+}
+// 文件更新处理
+const handleFileUpdate = (newValue, item, field) => {
+    item[field] = newValue
+}
+// 暴露方法
+defineExpose({
+    open,
+    close
+});
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.dialog-container {
+    width: 80vw;
+    max-width: px2rpx(800);
+    max-height: 85vh;
+    padding: px2rpx(24);
+    overflow: hidden;
+    border-radius: px2rpx(12);
+
+    .dialog-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: px2rpx(24);
+        padding-bottom: px2rpx(16);
+        border-bottom: 1px solid #f3f4f6;
+
+        .dialog-title {
+            font-size: px2rpx(18);
+            font-weight: 600;
+            color: #1f2937;
+        }
+
+        .dialog-close {
+            width: px2rpx(32);
+            height: px2rpx(32);
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: px2rpx(28);
+            color: #9ca3af;
+            cursor: pointer;
+            transition: all 0.3s;
+
+            &:hover {
+                color: #1f2937;
+            }
+        }
+    }
+
+    .dialog-footer {
+        display: flex;
+        gap: px2rpx(12);
+        justify-content: flex-end;
+        padding-top: px2rpx(16);
+        border-top: 1px solid #f3f4f6;
+
+        .btn {
+            min-width: px2rpx(120);
+            padding: px2rpx(12) px2rpx(24);
+            border-radius: px2rpx(6);
+            font-size: px2rpx(14);
+            font-weight: 600;
+            border: none;
+            cursor: pointer;
+            text-align: center;
+            transition: all 0.3s;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+
+            &.btn-cancel {
+                background: #f3f4f6;
+                color: #6b7280;
+
+                &:hover {
+                    background: #e5e7eb;
+                }
+
+                &:active {
+                    background: #d1d5db;
+                }
+            }
+
+            &.btn-confirm {
+                background: #ea2027;
+                color: #fff;
+
+                &:hover {
+                    background: #d11920;
+                }
+
+                &:active {
+                    background: #c01819;
+                }
+            }
+        }
+    }
+
+    .crm-form {
+        overflow-y: auto;
+        max-height: 70vh;
+    }
+}
+</style>

+ 1049 - 0
pages/mine/components/BankInfoTab.vue

@@ -0,0 +1,1049 @@
+<template>
+    <view class="user-form crm-form">
+        <uni-row class="demo-uni-row uni-row1">
+            <uni-col :xs="24" :sm="24" :md="24" :lg="6" :xl="6">
+                <view class="bank-menu">
+                    <view v-for="item in bankTypes" :key="item.key" class="bank-menu-item"
+                        :class="{ active: selectedBankType === item.key }" @click="selectedBankType = item.key">
+                        <image class="bank-icon" :src="item.icon" mode="widthFix" />
+                        <text>{{ item.label }}</text>
+                    </view>
+                </view>
+            </uni-col>
+            <uni-col :xs="24" :sm="24" :md="24" :lg="18" :xl="18">
+                <view class="bank-content" v-if="selectedBankType === 'crypto'">
+                    <view class="bank-info" v-for="(item, index) in cryptoWallets" :key="item.id">
+                        <view class="card-header">
+                            <view class="bank-header">
+                                <uni-row style="width: 100%;">
+                                    <uni-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+                                        <view class="bank-title">
+                                            <cwg-icon v-if="item.defaultBank" name="crm-star" color="#ea2027" />
+                                            <text>{{ currentBankType?.label }} {{ index + 1 }} </text>
+                                            <text v-if="item.defaultBank" v-t="'PersonalManagement.Title.Default'" />
+                                        </view>
+                                    </uni-col>
+                                    <uni-col :xs="24" :sm="24" :md="16" :lg="16" :xl="16">
+                                        <view class="bank-actions">
+                                            <view class="action-btn bg-secondary" v-if="item.authStatus == 0"
+                                                type="primary" v-t="'State.ToCertified'"
+                                                @click="doReady(item.id, item)" />
+                                            <view class="action-btn bg-secondary" v-if="item.authStatus == 0"
+                                                type="primary" @click="openCardDialog(item.id, item)"
+                                                v-t="'PersonalManagement.CardVerify.Title'" />
+                                            <view class="action-btn bg-secondary"
+                                                v-if="!editingId && item.authStatus !== 1" @tap="startEdit(item)"
+                                                v-t="'Btn.Editor'" />
+                                            <template v-if="editingId === item.id">
+                                                <view class="action-btn bg-secondary" @tap="saveBank(item)"
+                                                    v-t="'Btn.Save'" />
+                                                <view class="action-btn bg-secondary" @tap="cancelEdit()"
+                                                    v-t="'Btn.Cancel'" />
+                                            </template>
+                                            <view class="action-btn delete" @tap="confirmDelete(item)"
+                                                v-t="'Btn.Delete'" />
+                                        </view>
+                                    </uni-col>
+                                </uni-row>
+                            </view>
+                        </view>
+                        <uni-forms :model="item" labelWidth="200" label-position="top">
+                            <uni-row class="demo-uni-row">
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('blockchain.item3')">
+                                        <uni-easyinput :clearable="false" v-model="item.addressName"
+                                            :disabled="editingId !== item.id"
+                                            :placeholder="locale == 'es' ? 'Introduzca el nombre de la red' : t('placeholder.input')" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('blockchain.item4')">
+                                        <uni-easyinput :clearable="false" v-model="item.address"
+                                            :disabled="editingId !== item.id"
+                                            :placeholder="locale == 'es' ? 'Introduzca la dirección de la billetera' : t('placeholder.input')" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24"
+                                    v-if="item.cardFiles && item.cardFiles.length">
+                                    <uni-forms-item :label="t('PersonalManagement.Label.CertificationPhoto')">
+                                        <view class="photo-upload">
+                                            <view v-for="(file, idx) in item.cardFiles" :key="idx" class="photo-item">
+                                                <image class="photo-preview" :src="updateUrl + file.path"
+                                                    mode="aspectFill" />
+                                            </view>
+                                        </view>
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24">
+                                    <uni-forms-item class="checkbox-item">
+                                        <uni-data-checkbox :disabled="editingId !== item.id" v-model="item.defaultBank1"
+                                            multiple :localdata="hobbys" />
+                                    </uni-forms-item>
+                                </uni-col>
+                            </uni-row>
+                        </uni-forms>
+                    </view>
+                    <view class="add-wallet-btn" @click="addBank()" v-if="cryptoWallets.length < 2">
+                        <text>+</text>
+                        <text>添加加密货币钱包</text>
+                    </view>
+                </view>
+                <view class="bank-content" v-if="selectedBankType === 'unionpay'">
+                    <view class="bank-info" v-for="(item, index) in unionpayCards" :key="item.id">
+                        <view class="card-header">
+                            <view class="bank-header">
+                                <uni-row style="width: 100%;">
+                                    <uni-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+                                        <view class="bank-title">
+                                            <cwg-icon v-if="item.defaultBank" name="crm-star" color="#ea2027" />
+                                            <text>{{ currentBankType?.label }}{{ index + 1 }} </text>
+                                            <text v-if="item.defaultBank" v-t="'PersonalManagement.Title.Default'" />
+                                        </view>
+                                    </uni-col>
+                                    <uni-col :xs="24" :sm="24" :md="16" :lg="16" :xl="16">
+                                        <view class="bank-actions">
+                                            <view class="action-btn bg-secondary"
+                                                v-if="!editingId && item.authStatus !== 1" @tap="startEdit(item)"
+                                                v-t="'Btn.Editor'" />
+                                            <template v-if="editingId === item.id">
+                                                <view class="action-btn bg-secondary" @tap="saveBank(item)"
+                                                    v-t="'Btn.Save'" />
+                                                <view class="action-btn bg-secondary" @tap="cancelEdit()"
+                                                    v-t="'Btn.Cancel'" />
+                                            </template>
+                                            <view class="action-btn delete" @tap="confirmDelete(item)"
+                                                v-t="'Btn.Delete'" />
+                                        </view>
+                                    </uni-col>
+                                </uni-row>
+                            </view>
+                        </view>
+                        <uni-forms :model="item" labelWidth="200" label-position="top">
+                            <uni-row class="demo-uni-row">
+                                <uni-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+                                    <uni-forms-item>
+                                        <cwg-file-picker v-model="item.bankFront" :editable="editingId === item.id"
+                                            :limit="1" uploadUrl="/custom/bank/upload" :baseUrl="updateUrl"
+                                            :imageWidth="150" :imageHeight="150" uploadText="点击上传" replaceText="点击替换"
+                                            noImageText="暂无图片" :showPreviewDelete="editingId === item.id"
+                                            @update:modelValue="(val) => handleFileUpdate(val, item, 'bankFront')" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('PersonalManagement.Label.BankAccountName')">
+                                        <uni-easyinput :clearable="false" v-model="item.bankUname" :disabled="true"
+                                            :placeholder="locale == 'es' ? 'Introduzca el nombre de la red' : t('placeholder.input')" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('PersonalManagement.Label.BankName')">
+                                        <cwg-combox :clearable="false" :filterable="true" v-model:value="item.bankName"
+                                            :options="bankOptions" :placeholder="t('placeholder.choose')"
+                                            :disabled="editingId !== item.id" @change="onStateChange" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('PersonalManagement.Label.BankAccount')">
+                                        <uni-easyinput :clearable="false" v-model="item.bankCardNum"
+                                            :disabled="editingId !== item.id"
+                                            :placeholder="locale == 'es' ? 'Introduzca la dirección de la billetera' : t('placeholder.input')" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('PersonalManagement.Label.AccountOpeningBranch')">
+                                        <uni-easyinput :clearable="false" v-model="item.bankBranchName"
+                                            :disabled="editingId !== item.id"
+                                            :placeholder="locale == 'es' ? 'Introduzca la dirección de la billetera' : t('placeholder.input')" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24">
+                                    <uni-forms-item class="checkbox-item">
+                                        <uni-data-checkbox :disabled="editingId !== item.id" v-model="item.defaultBank1"
+                                            multiple :localdata="hobbys" />
+                                    </uni-forms-item>
+                                </uni-col>
+
+                            </uni-row>
+                        </uni-forms>
+                    </view>
+                    <view class="add-wallet-btn" @click="addBank()" v-if="unionpayCards.length < 2">
+                        <text>+</text>
+                        <text>添加加密货币钱包</text>
+                    </view>
+                </view>
+                <view class="bank-content" v-if="selectedBankType === 'bank'">
+                    <view class="bank-info" v-for="(item, index) in wireTransfers" :key="item.id">
+                        <view class="card-header">
+                            <view class="bank-header">
+                                <uni-row style="width: 100%;">
+                                    <uni-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+                                        <view class="bank-title">
+                                            <cwg-icon v-if="item.defaultBank" name="crm-star" color="#ea2027" />
+                                            <text>{{ currentBankType?.label }} {{ index + 1 }} </text>
+                                            <text v-if="item.defaultBank" v-t="'PersonalManagement.Title.Default'" />
+                                        </view>
+                                    </uni-col>
+                                    <uni-col :xs="24" :sm="24" :md="16" :lg="16" :xl="16">
+                                        <view class="bank-actions">
+                                            <view class="action-btn bg-secondary"
+                                                v-if="!editingId && item.authStatus !== 1" @tap="startEdit(item)"
+                                                v-t="'Btn.Editor'" />
+                                            <template v-if="editingId === item.id">
+                                                <view class="action-btn bg-secondary" @tap="saveBank(item)"
+                                                    v-t="'Btn.Save'" />
+                                                <view class="action-btn bg-secondary" @tap="cancelEdit()"
+                                                    v-t="'Btn.Cancel'" />
+                                            </template>
+                                            <view class="action-btn delete" @tap="confirmDelete(item)"
+                                                v-t="'Btn.Delete'" />
+                                        </view>
+                                    </uni-col>
+                                </uni-row>
+                            </view>
+                        </view>
+                        <uni-forms :model="item" labelWidth="200" label-position="top">
+                            <uni-row class="demo-uni-row">
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('PersonalManagement.Label.BankAccountName')">
+                                        <uni-easyinput :clearable="false" v-model="item.bankUname" :disabled="true"
+                                            :placeholder="locale == 'es' ? 'Introduzca el nombre de la red' : t('placeholder.input')" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('PersonalManagement.Label.BankAccount')">
+                                        <uni-easyinput :clearable="false" v-model="item.bankCardNum"
+                                            :disabled="editingId !== item.id"
+                                            :placeholder="locale == 'es' ? 'Introduzca la dirección de la billetera' : t('placeholder.input')" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('PersonalManagement.Label.BankName')">
+                                        <uni-easyinput :clearable="false" v-model="item.bankName"
+                                            :disabled="editingId !== item.id"
+                                            :placeholder="locale == 'es' ? 'Introduzca el nombre del banco' : t('placeholder.input')" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('PersonalManagement.Label.BankAddress')">
+                                        <uni-easyinput :clearable="false" v-model="item.bankAddr"
+                                            :placeholder="locale == 'es' ? 'Introduzca la dirección del banco' : t('placeholder.input')"
+                                            :disabled="editingId !== item.id" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('PersonalManagement.Label.SwiftBIC')">
+                                        <uni-easyinput :clearable="false" v-model="item.swiftCode"
+                                            :placeholder="locale == 'es' ? 'Introduzca el SWIFT/BIC' : t('placeholder.input')"
+                                            :disabled="editingId !== item.id" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('PersonalManagement.Label.BankCode')">
+                                        <uni-easyinput :clearable="false" v-model="item.bankCode"
+                                            :placeholder="locale == 'es' ? 'Introduzca el código del banco' : t('placeholder.input')"
+                                            :disabled="editingId !== item.id" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item
+                                        :label="locale == 'es' ? 'Número de sucursal (opcional)' : 'Account Agency NO'">
+                                        <uni-easyinput :clearable="false" v-model="item.agencyNo"
+                                            :placeholder="locale == 'es' ? 'Introduzca el número de sucursal' : t('placeholder.input')"
+                                            :disabled="editingId !== item.id" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24">
+                                    <uni-forms-item class="checkbox-item">
+                                        <uni-data-checkbox :disabled="editingId !== item.id" v-model="item.defaultBank1"
+                                            multiple :localdata="hobbys" />
+                                    </uni-forms-item>
+                                </uni-col>
+                            </uni-row>
+                        </uni-forms>
+                    </view>
+                    <view class="add-wallet-btn" @click="addBank()" v-if="wireTransfers.length < 2">
+                        <text>+</text>
+                        <text>添加加密货币钱包</text>
+                    </view>
+                </view>
+                <view class="bank-content" v-if="selectedBankType === 'credit'">
+                    <view class="bank-info" v-for="(item, index) in creditCards" :key="item.id">
+                        <view class="card-header">
+                            <view class="bank-header">
+                                <uni-row style="width: 100%;">
+                                    <uni-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+                                        <view class="bank-title">
+                                            <cwg-icon v-if="item.defaultBank" name="crm-star" color="#ea2027" />
+                                            <text>{{ currentBankType?.label }} {{ index + 1 }} </text>
+                                            <text v-if="item.defaultBank" v-t="'PersonalManagement.Title.Default'" />
+                                        </view>
+                                    </uni-col>
+                                    <uni-col :xs="24" :sm="24" :md="16" :lg="16" :xl="16">
+                                        <view class="bank-actions">
+                                            <view class="action-btn bg-secondary"
+                                                v-if="!editingId && item.authStatus !== 1" @tap="startEdit(item)"
+                                                v-t="'Btn.Editor'" />
+                                            <template v-if="editingId === item.id">
+                                                <view class="action-btn bg-secondary" @tap="saveBank(item)"
+                                                    v-t="'Btn.Save'" />
+                                                <view class="action-btn bg-secondary" @tap="cancelEdit()"
+                                                    v-t="'Btn.Cancel'" />
+                                            </template>
+                                            <view class="action-btn delete" @tap="confirmDelete(item)"
+                                                v-t="'Btn.Delete'" />
+                                        </view>
+                                    </uni-col>
+                                </uni-row>
+                            </view>
+                        </view>
+                        <uni-forms :model="item" labelWidth="200" label-position="top">
+                            <uni-row class="demo-uni-row">
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('PersonalManagement.Label.CreditCardAccountName')">
+                                        <uni-easyinput :clearable="false" v-model="item.bankUname" :disabled="true"
+                                            :placeholder="t('placeholder.input')" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('PersonalManagement.Label.CreditCardAccount')">
+                                        <uni-easyinput :clearable="false" v-model="item.bankCardNum"
+                                            :disabled="editingId !== item.id"
+                                            :placeholder="locale == 'es' ? 'Introduzca el número de tarjeta' : t('placeholder.input')" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('PersonalManagement.Label.ExpirationYear')">
+                                        <uni-easyinput :clearable="false" v-model="item.expiryYearMonth"
+                                            :disabled="editingId !== item.id"
+                                            :placeholder="locale == 'es' ? 'Introduzca MM/AA' : t('placeholder.input')" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('CVV')">
+                                        <uni-easyinput :clearable="false" v-model="item.cvv"
+                                            :disabled="editingId !== item.id"
+                                            :placeholder="locale == 'es' ? 'Introduzca el CVV' : t('placeholder.input')" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24">
+                                    <uni-forms-item class="checkbox-item">
+                                        <uni-data-checkbox :disabled="editingId !== item.id" v-model="item.defaultBank1"
+                                            multiple :localdata="hobbys" />
+                                    </uni-forms-item>
+                                </uni-col>
+
+                            </uni-row>
+                        </uni-forms>
+                    </view>
+                    <view class="add-wallet-btn" @click="addBank()">
+                        <text>+</text>
+                        <text>添加加密货币钱包</text>
+                    </view>
+                </view>
+            </uni-col>
+        </uni-row>
+    </view>
+    <!-- 删除确认弹窗 -->
+    <uni-popup ref="deletePopup" type="dialog">
+        <uni-popup-dialog :title="t('Msg.SystemPrompt')" :content="t('Msg.Delete')" @confirm="deleteBank"
+            @close="closeDeletePopup" />
+    </uni-popup>
+    <!-- 新增银行弹窗 -->
+    <add-bank-dialog ref="addBankDialogRef" @success="addSuccess" />
+    <!-- kyc认证弹窗 -->
+    <kyc-auth-dialog ref="kycDialogRef" />
+    <!-- 证件认证弹窗 -->
+    <card-auth-dialog ref="cardDialogRef" />
+</template>
+
+<script setup lang="ts">
+import { computed, ref, onMounted } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { personalApi } from '@/service/personal';
+import KycAuthDialog from './KycAuthDialog.vue';
+import CardAuthDialog from './CardAuthDialog.vue';
+import AddBankDialog from './AddBankDialog.vue';
+import config from '@/config'
+import { BankType } from './bank'
+const { t, locale } = useI18n();
+interface BankListType {
+    key: string;
+    label: string;
+    icon: string;
+}
+
+interface BankInfo {
+    id?: string;
+    type: number;
+    defaultBank: boolean;
+    approveStatus?: number;
+    authStatus?: number;
+    blockchainName: string;
+    walletAddress: string;
+    photos?: string[];
+    expiryYearMonth?: string;
+    expiryYear?: string;
+    expiryMonth?: string;
+    [key: string]: any;
+}
+const selectedBankType = ref<string>('crypto');
+const bankTypes = computed<BankListType[]>(() => [
+    { key: 'crypto', label: t('blockchain.item2'), icon: '/static/images/info/bank-info_1.png' },
+    { key: 'unionpay', label: t('PersonalManagement.Title.ChinaUnionPayCard'), icon: '/static/images/info/bank-info_2.png' },
+    { key: 'bank', label: t('PersonalManagement.Title.BankWireTransfer'), icon: '/static/images/info/bank-info_3.png' },
+    { key: 'credit', label: t('PersonalManagement.Label.CreditCard'), icon: '/static/images/info/bank-info_4.png' }
+]);
+const currentBankType = computed(() => bankTypes.value.find((item: BankListType) => item.key === selectedBankType.value));
+const hobbys = computed(() => {
+    switch (selectedBankType.value) {
+        case 'crypto':
+            return [{ value: 1, text: t('blockchain.item8') }]
+        case 'unionpay':
+            return [{ value: 1, text: t('PersonalManagement.Title.DefaultBank') }]
+        case 'bank':
+            return [{ value: 1, text: t('PersonalManagement.Title.DefaultWire') }]
+        case 'credit':
+            return [{ value: 1, text: t('PersonalManagement.Title.DefaultCredit') }]
+    }
+});
+// 状态
+const updateUrl = config.Host80
+const editingId = ref(null)
+const bankList = ref([])
+const isZh = computed(() => ['cn', 'zh', 'zhHant'].includes(locale.value));
+
+const getLangName = (item: any) => (isZh.value ? item.name : item.enName);
+const createOptions = (list: any[], valueKey = 'code') => {
+    return list.map((item) => ({
+        text: getLangName(item),
+        value: getLangName(item)
+    }));
+};
+
+const bankOptions = computed(() => createOptions(bankList.value, 'name'));
+// 银行数据
+const bankData = ref({
+    [BankType.CRYPTO]: [],
+    [BankType.UNIONPAY]: [],
+    [BankType.WIRE_TRANSFER]: [],
+    [BankType.CREDIT_CARD]: []
+})
+
+// 计算属性 - 各类型银行列表
+const cryptoWallets = computed(() => bankData.value[BankType.CRYPTO] || [])
+const unionpayCards = computed(() => bankData.value[BankType.UNIONPAY] || [])
+const wireTransfers = computed(() => bankData.value[BankType.WIRE_TRANSFER] || [])
+const creditCards = computed(() => bankData.value[BankType.CREDIT_CARD] || [])
+
+
+
+// 弹出层ref
+const deletePopup = ref(null)
+const deleteTarget = ref(null)
+
+
+// 开始编辑
+const startEdit = (item) => {
+    editingId.value = item.id
+}
+
+// 取消编辑
+const cancelEdit = () => {
+    editingId.value = null
+    getBankInfo() // 重新加载数据
+}
+
+// 保存银行信息
+const saveBank = async (item) => {
+
+    if (selectedBankType.value === 'crypto' && item.approveStatus == 1) {
+        uni.showToast({
+            title: "加密钱包认证审核中",
+            icon: "none"
+        });
+        return;
+    }
+    try {
+        const params = { ...item }
+        params.defaultBank = params.defaultBank1[0] ? 1 : 0
+
+        if (params.type === BankType.CREDIT_CARD && params.expiryYearMonth) {
+            const [year, month] = params.expiryYearMonth.split('/')
+            params.expiryYear = year
+            params.expiryMonth = month
+        }
+        const res = await personalApi.customBankUpdate(params)
+        if (res.code === 200) {
+            uni.hideLoading()
+            uni.showToast({
+                title: t('Msg.Success'),
+                icon: 'success'
+            })
+            editingId.value = null
+            getBankInfo()
+        } else {
+            throw new Error(res.msg)
+        }
+    } catch (error) {
+        uni.hideLoading()
+        uni.showToast({
+            title: error.message || t('Msg.Fail'),
+            icon: 'none'
+        })
+    }
+}
+// 删除
+const confirmDelete = (item) => {
+    if (selectedBankType.value === 'crypto' && item.approveStatus == 1) {
+        uni.showToast({
+            title: '加密钱包认证审核中',
+            icon: 'none'
+        })
+        return
+    }
+    deleteTarget.value = item
+    deletePopup.value.open()
+}
+
+// 关闭删除弹窗
+const closeDeletePopup = () => {
+    deletePopup.value.close()
+    deleteTarget.value = null
+}
+
+// 执行删除
+const deleteBank = async () => {
+    if (!deleteTarget.value) return
+    try {
+        const res = await personalApi.customBankDelete({
+            ids: [deleteTarget.value.id]
+        })
+        if (res.code === 200) {
+            uni.hideLoading()
+            uni.showToast({
+                title: t('Msg.DeleteSuccess'),
+                icon: 'success'
+            })
+            deletePopup.value.close()
+            deleteTarget.value = null
+            getBankInfo()
+        } else {
+            uni.showToast({
+                title: res.msg,
+                icon: 'none'
+            })
+        }
+    } catch (error) {
+        uni.hideLoading()
+        uni.showToast({
+            title: error.msg,
+            icon: 'none'
+        })
+        deletePopup.value.close()
+    }
+}
+// 新增银行信息
+const addBankDialogRef = ref(null);
+function addBank() {
+    switch (selectedBankType.value) {
+        case 'crypto':
+            openAddCrypto()
+            break;
+        case 'unionpay':
+            openAddUnionpay()
+            break;
+        case 'bank':
+            openAddBank()
+            break;
+        case 'credit':
+            openAddCredit()
+            break;
+    }
+}
+function openAddCrypto() {
+    const wallets = cryptoWallets.value || [];
+    // 1️⃣ 没有钱包
+    if (wallets.length === 0) {
+        addBankDialogRef.value?.open(4);
+        return;
+    }
+    // 2️⃣ 是否存在未认证钱包
+    const hasUnAuth = wallets.some(
+        item => item.authStatus === 0 || item.approveStatus === 1
+    );
+    if (hasUnAuth) {
+        uni.showToast({
+            title: "加密钱包未认证",
+            icon: "none"
+        });
+        return;
+    }
+    // 3️⃣ 是否达到上限
+    if (wallets.length >= 2) {
+        uni.showToast({
+            title: t('blockchain.item9'),
+            icon: "none"
+        });
+        return;
+    }
+    // 4️⃣ 正常打开
+    addBankDialogRef.value?.open(4);
+}
+function openAddUnionpay() {
+    const wallets = unionpayCards.value || [];
+    if (wallets.length === 0) {
+        addBankDialogRef.value?.open(1);
+        return;
+    }
+    if (wallets.length >= 2) {
+        uni.showToast({
+            title: t('Msg.UnionPayCARDS'),
+            icon: "none"
+        });
+        return;
+    }
+    addBankDialogRef.value?.open(1);
+}
+function openAddBank() {
+    const wallets = wireTransfers.value || [];
+    if (wallets.length === 0) {
+        addBankDialogRef.value?.open(2);
+        return;
+    }
+    if (wallets.length >= 2) {
+        uni.showToast({
+            title: t('Msg.WireTransfers'),
+            icon: "none"
+        });
+        return;
+    }
+    addBankDialogRef.value?.open(2);
+}
+function openAddCredit() {
+    const wallets = creditCards.value || [];
+    if (wallets.length === 0) {
+        addBankDialogRef.value?.open(3);
+        return;
+    }
+    addBankDialogRef.value?.open(3);
+}
+// 新增银行信息成功回调
+const addSuccess = (e) => {
+    if (selectedBankType.value === 'crypto') {
+        openKycDialog(e)
+    }
+    getBankInfo();
+}
+
+// kyc认证和证件认证
+const kycDialogRef = ref(null);
+const cardDialogRef = ref(null);
+// kyc认证
+const doReady = (bankId, item) => {
+    if (item.approveStatus == 1) {
+        uni.showToast({
+            title: "加密钱包认证审核中",
+            icon: "none"
+        });
+        return;
+    }
+    if (item.authStatus == 1) {
+        uni.showToast({
+            title: "加密钱包已认证",
+            icon: "none"
+        });
+        return;
+    }
+    openKycDialog(bankId)
+}
+// 打开kyc认证弹窗
+function openKycDialog(e) {
+    kycDialogRef.value?.open(e);
+}
+// 打开证件认证弹窗
+function openCardDialog(e, item) {
+    if (item.approveStatus == 1) {
+        uni.showToast({
+            title: "加密钱包认证审核中",
+            icon: "none"
+        });
+        return;
+    }
+    if (item.authStatus == 1) {
+        uni.showToast({
+            title: "加密钱包已认证",
+            icon: "none"
+        });
+        return;
+    }
+    cardDialogRef.value?.open(e, item);
+}
+
+
+// 掩码卡号
+const maskCardNumber = (num) => {
+    if (!num) return '--'
+    if (num.length <= 4) return num
+    return '**** **** **** ' + num.slice(-4)
+}
+
+// 获取银行列表
+const getBankList = async () => {
+    const res = await personalApi.BankList({})
+    if (res.code === 200) {
+        bankList.value = res.data
+    }
+}
+// 文件更新处理
+const handleFileUpdate = (newValue, item, field) => {
+    item[field] = newValue
+}
+
+// 或者如果你需要在上传成功后做其他操作
+const handleUploadComplete = (result, item, field) => {
+    if (result.success) {
+        console.log('上传完成:', result.paths)
+        // 可以在这里做其他操作,比如保存到服务器
+    }
+}
+// 获取银行信息
+const getBankInfo = async () => {
+    try {
+        const res = await personalApi.customBankList({})
+        if (res.code === 200) {
+            // 清空数据
+            bankData.value = {
+                [BankType.CRYPTO]: [],
+                [BankType.UNIONPAY]: [],
+                [BankType.WIRE_TRANSFER]: [],
+                [BankType.CREDIT_CARD]: []
+            }
+            // 分类数据
+            res.data.forEach(item => {
+                item.defaultBank1 = item.defaultBank == 1 ? [1] : []
+                if (item.type === BankType.UNIONPAY) {
+                    bankData.value[BankType.UNIONPAY].push(item)
+                } else if (item.type === BankType.WIRE_TRANSFER) {
+                    bankData.value[BankType.WIRE_TRANSFER].push(item)
+                } else if (item.type === BankType.CREDIT_CARD) {
+                    item.expiryYearMonth = item.expiryYear && item.expiryMonth
+                        ? `${item.expiryYear}/${item.expiryMonth}`
+                        : ''
+                    bankData.value[BankType.CREDIT_CARD].push(item)
+                } else if (item.type === BankType.CRYPTO) {
+                    bankData.value[BankType.CRYPTO].push(item)
+                }
+            })
+        }
+    } catch (error) {
+        uni.showToast({
+            title: error.message || t('Msg.Fail'),
+            icon: 'none'
+        })
+    }
+}
+// 获取银行信息
+onMounted(() => {
+    getBankList()
+    getBankInfo();
+});
+</script>
+
+
+
+<style scoped lang="scss">
+.user-form {
+    margin-top: px2rpx(30);
+}
+
+:deep(.uni-row1) {
+    .uni-col {
+        padding: 0 10px !important;
+    }
+
+    .uni-forms-item {
+        min-height: px2rpx(79);
+        margin-bottom: px2rpx(10);
+    }
+
+    .uni-easyinput__content {
+        border: none !important;
+        background-color: var(--color-zinc-100) !important;
+    }
+}
+
+.bank-menu {
+    background: #fff;
+    overflow: hidden;
+
+    .bank-menu-item {
+        display: flex;
+        align-items: center;
+        gap: px2rpx(12);
+        padding: px2rpx(10) px2rpx(16);
+        cursor: pointer;
+        border: 1px solid #f3f4f6;
+        font-size: px2rpx(16);
+        font-weight: 500;
+        color: #1f2937;
+        transition: all 0.3s;
+        height: px2rpx(50);
+        border-radius: px2rpx(8);
+        margin-bottom: px2rpx(8);
+
+        .bank-icon {
+            width: px2rpx(50);
+        }
+
+        &.active {
+            background: #ea2027;
+            color: #fff;
+            border-radius: px2rpx(0);
+        }
+
+        &:hover {
+            background: #f9fafb;
+        }
+
+        &.active:hover {
+            background: #d11920;
+        }
+    }
+}
+
+.bank-content {
+    background: #fff;
+    border-radius: px2rpx(8);
+    padding: 0 px2rpx(24);
+
+    .bank-info {
+        .bank-header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            margin-bottom: px2rpx(24);
+            padding-bottom: px2rpx(16);
+            border-bottom: 1px solid #f3f4f6;
+
+            .bank-title {
+                display: flex;
+                align-items: center;
+                font-size: px2rpx(20);
+                font-weight: 600;
+                color: #1f2937;
+                margin-bottom: px2rpx(20);
+            }
+
+            .bank-actions {
+                display: flex;
+                gap: px2rpx(12);
+                align-items: center;
+                justify-content: flex-end;
+
+                .action-btn {
+                    padding: px2rpx(8) px2rpx(16);
+                    border: 1px solid #d1d5db;
+                    border-radius: px2rpx(4);
+                    font-size: px2rpx(14);
+                    cursor: pointer;
+                    background: #fff;
+                    transition: all 0.3s;
+
+                    &:hover {
+                        background: #f3f4f6;
+                    }
+
+                    &.delete {
+                        background: #ea2027;
+                        color: #fff;
+                        border-color: #ea2027;
+
+                        &:hover {
+                            background: #d11920;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    .photo-upload {
+        display: flex;
+        gap: px2rpx(16);
+
+        .photo-item {
+            width: px2rpx(120);
+            height: px2rpx(120);
+            border-radius: px2rpx(8);
+            overflow: hidden;
+            background: #f3f4f6;
+
+            .photo-preview {
+                width: 100%;
+                height: 100%;
+            }
+        }
+    }
+
+    .add-wallet-btn {
+        margin-top: px2rpx(24);
+        height: px2rpx(48);
+        background: #ea2027;
+        color: #fff;
+        border-radius: px2rpx(4);
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: px2rpx(8);
+        font-size: px2rpx(16);
+        font-weight: 600;
+        cursor: pointer;
+        transition: all 0.3s;
+
+        &:hover {
+            background: #d11920;
+        }
+    }
+
+    .bankFront {
+        width: 100%;
+
+        // 上传包装器
+        .upload-wrapper {
+            :deep(.uni-file-picker) {
+                .file-picker__box {
+                    border: none;
+                    background: transparent;
+                }
+
+                // 文件列表容器
+                .file-picker__box-list {
+                    display: flex;
+                    gap: px2rpx(16);
+                    flex-wrap: wrap;
+
+                    // 已上传的文件项
+                    .file-picker__box-item {
+                        width: px2rpx(300) !important;
+                        height: px2rpx(300) !important;
+                        margin: 0;
+                        border: 1px solid #e5e7eb;
+                        border-radius: px2rpx(8);
+                        overflow: hidden;
+                        position: relative;
+
+                        // 图片预览
+                        .file-picker__box-item-image {
+                            width: 100%;
+                            height: 100%;
+                            object-fit: cover;
+                        }
+
+                        // 删除按钮
+                        .file-picker__box-item-close {
+                            position: absolute;
+                            top: px2rpx(4);
+                            right: px2rpx(4);
+                            width: px2rpx(24);
+                            height: px2rpx(24);
+                            background: rgba(0, 0, 0, 0.5);
+                            border-radius: 50%;
+                            display: flex;
+                            align-items: center;
+                            justify-content: center;
+
+                            &::before {
+                                content: '×';
+                                color: #fff;
+                                font-size: px2rpx(28);
+                                line-height: 1;
+                            }
+
+                            .close-icon {
+                                display: none;
+                            }
+                        }
+                    }
+                }
+
+                // 上传按钮
+                .file-picker__box-add {
+                    width: px2rpx(300) !important;
+                    height: px2rpx(300) !important;
+                    margin: 0;
+                    border: 2rpx dashed #d1d5db;
+                    border-radius: px2rpx(8);
+                    background: #f9fafb;
+                    transition: all 0.3s;
+
+                    &:hover {
+                        border-color: #ea2027;
+                        background: #fef2f2;
+                    }
+
+                    // 自定义上传按钮内容
+                    .custom-upload-btn {
+                        width: 100%;
+                        height: 100%;
+                        display: flex;
+                        flex-direction: column;
+                        align-items: center;
+                        justify-content: center;
+
+                        .plus {
+                            font-size: px2rpx(48);
+                            color: #9ca3af;
+                            line-height: 1;
+                            margin-bottom: px2rpx(8);
+                        }
+
+                        .tip {
+                            font-size: px2rpx(24);
+                            color: #6b7280;
+                        }
+                    }
+
+                    // 隐藏默认的加号
+                    .add-icon {
+                        display: none;
+                    }
+                }
+
+                // 上传进度
+                .file-picker__box-progress {
+                    width: px2rpx(300);
+                    height: px2rpx(300);
+                    border-radius: px2rpx(8);
+                    background: rgba(0, 0, 0, 0.5);
+                    color: #fff;
+                }
+            }
+        }
+
+        // 图片预览
+        .image-preview {
+            .preview-image {
+                width: px2rpx(300);
+                height: px2rpx(300);
+                border-radius: px2rpx(8);
+                object-fit: cover;
+                cursor: pointer;
+                border: 1px solid #e5e7eb;
+            }
+
+            .no-image {
+                width: px2rpx(300);
+                height: px2rpx(300);
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                background: #f5f5f5;
+                border-radius: px2rpx(8);
+                color: #999;
+                font-size: px2rpx(24);
+                border: 1px solid #e5e7eb;
+            }
+        }
+    }
+}
+</style>

+ 583 - 0
pages/mine/components/BankItem.vue

@@ -0,0 +1,583 @@
+<template>
+    <view class="user-form crm-form">
+        <uni-row class="demo-uni-row uni-row1">
+            <uni-col :xs="24" :sm="24" :md="24" :lg="6" :xl="6">
+                <view class="bank-menu">
+                    <view v-for="item in bankTypes" :key="item.key" class="bank-menu-item"
+                        :class="{ active: selectedBankType === item.key }" @click="selectedBankType = item.key">
+                        <image class="bank-icon" :src="item.icon" mode="widthFix" />
+                        <text>{{ item.label }}</text>
+                    </view>
+                </view>
+            </uni-col>
+            <uni-col :xs="24" :sm="24" :md="24" :lg="18" :xl="18">
+                <view class="bank-content" v-if="selectedBankType === 'crypto'">
+                    <view class="bank-info" v-for="(item, index) in cryptoWallets" :key="item.id">
+                        <view class="card-header">
+                            <view class="bank-header">
+                                <uni-row style="width: 100%;">
+                                    <uni-col :xs="24" :sm="24" :md="8" :lg="8" :xl="8">
+                                        <view class="bank-title">
+                                            <cwg-icon v-if="item.defaultBank" name="crm-star" color="#ea2027" />
+                                            <text>{{ currentBankType?.label }} {{ index + 1 }} </text>
+                                            <text v-if="item.defaultBank" v-t="'PersonalManagement.Title.Default'" />
+                                        </view>
+                                    </uni-col>
+                                    <uni-col :xs="24" :sm="24" :md="16" :lg="16" :xl="16">
+                                        <view class="bank-actions">
+                                            <view class="action-btn bg-secondary" v-if="item.authStatus == 0"
+                                                type="primary" v-t="'State.ToCertified'"
+                                                @click="doReady(item.id, item)" />
+                                            <view class="action-btn bg-secondary" v-if="item.authStatus == 0"
+                                                type="primary" @click="openCardDialog(item.id, item)"
+                                                v-t="'PersonalManagement.CardVerify.Title'" />
+                                            <view class="action-btn bg-secondary"
+                                                v-if="!editingId && item.authStatus !== 1" @tap="startEdit(item)"
+                                                v-t="'Btn.Editor'" />
+                                            <template v-if="editingId === item.id">
+                                                <view class="action-btn bg-secondary" @tap="saveBank(item)"
+                                                    v-t="'Btn.Save'" />
+                                                <view class="action-btn bg-secondary" @tap="cancelEdit()"
+                                                    v-t="'Btn.Cancel'" />
+                                            </template>
+                                            <view class="action-btn delete" @tap="confirmDelete(item)"
+                                                v-t="'Btn.Delete'" />
+                                        </view>
+                                    </uni-col>
+                                </uni-row>
+                            </view>
+                        </view>
+                        <uni-forms :model="item" labelWidth="200" label-position="top">
+                            <uni-row class="demo-uni-row">
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('blockchain.item3')">
+                                        <uni-easyinput :clearable="false" v-model="item.addressName"
+                                            :disabled="editingId !== item.id"
+                                            :placeholder="locale == 'es' ? 'Introduzca el nombre de la red' : t('placeholder.input')" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
+                                    <uni-forms-item :label="t('blockchain.item4')">
+                                        <uni-easyinput :clearable="false" v-model="item.address"
+                                            :disabled="editingId !== item.id"
+                                            :placeholder="locale == 'es' ? 'Introduzca la dirección de la billetera' : t('placeholder.input')" />
+                                    </uni-forms-item>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24"
+                                    v-if="item.cardFiles && item.cardFiles.length">
+                                    <uni-forms-item :label="t('PersonalManagement.Label.CertificationPhoto')">
+                                        <view class="photo-upload">
+                                            <view v-for="(file, idx) in item.cardFiles" :key="idx" class="photo-item">
+                                                <image class="photo-preview" :src="updateUrl + file.path"
+                                                    mode="aspectFill" />
+                                            </view>
+                                        </view>
+                                    </uni-forms-item>
+                                </uni-col>
+                            </uni-row>
+                        </uni-forms>
+                    </view>
+                    <view class="add-wallet-btn" @click="openAddBankCard" v-if="cryptoWallets.length < 2">
+                        <text>+</text>
+                        <text>添加加密货币钱包</text>
+                    </view>
+                </view>
+                <view class="bank-content" v-if="selectedBankType === 'unionpay'">
+                    <view class="bank-info" v-for="item in unionpayCards" :key="item.id">
+                    </view>
+                    <view class="add-wallet-btn" @click="openAddBankCard">
+                        <text>+</text>
+                        <text>{{ t('ImproveImmediately.Label.AddCryptoWallet') }}</text>
+                    </view>
+                </view>
+            </uni-col>
+        </uni-row>
+    </view>
+    <!-- 删除确认弹窗 -->
+    <uni-popup ref="deletePopup" type="dialog">
+        <uni-popup-dialog :title="t('Msg.SystemPrompt')" :content="t('Msg.Delete')" @confirm="deleteBank"
+            @close="closeDeletePopup" />
+    </uni-popup>
+    <!-- 新增银行弹窗 -->
+    <cwg-add-bank-dialog ref="addBankDialogRef" @success="addSuccess" />
+    <!-- kyc认证弹窗 -->
+    <kyc-dialog ref="kycDialogRef" />
+    <!-- 证件认证弹窗 -->
+    <card-dialog ref="cardDialogRef" />
+</template>
+
+<script setup lang="ts">
+import { computed, ref, onMounted } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { personalApi } from '@/service/personal';
+import KycDialog from './KycAuthDialog.vue';
+import CardDialog from './CardAuthDialog.vue';
+import config from '@/config'
+import { BankType, AuthStatus, ApproveStatus } from './bank'
+const { t } = useI18n();
+interface BankListType {
+    key: string;
+    label: string;
+    icon: string;
+}
+
+interface BankInfo {
+    id?: string;
+    type: number;
+    defaultBank: boolean;
+    approveStatus?: number;
+    authStatus?: number;
+    blockchainName: string;
+    walletAddress: string;
+    photos?: string[];
+    expiryYearMonth?: string;
+    expiryYear?: string;
+    expiryMonth?: string;
+    [key: string]: any;
+}
+const selectedBankType = ref<string>('crypto');
+const bankTypes = computed<BankListType[]>(() => [
+    { key: 'crypto', label: t('blockchain.item2'), icon: '/static/images/info/bank-info_1.png' },
+    { key: 'unionpay', label: t('PersonalManagement.Title.ChinaUnionPayCard'), icon: '/static/images/info/bank-info_2.png' },
+    { key: 'bank', label: t('PersonalManagement.Title.BankWireTransfer'), icon: '/static/images/info/bank-info_3.png' },
+    { key: 'credit', label: t('PersonalManagement.Label.CreditCard'), icon: '/static/images/info/bank-info_4.png' }
+]);
+const currentBankType = computed(() => bankTypes.value.find((item: BankListType) => item.key === selectedBankType.value));
+
+// 状态
+const updateUrl = config.Host80
+const editingId = ref(null)
+const bankOptions = ref([])
+
+// 银行数据
+const bankData = ref({
+    [BankType.CRYPTO]: [],
+    [BankType.UNIONPAY]: [],
+    [BankType.WIRE_TRANSFER]: [],
+    [BankType.CREDIT_CARD]: []
+})
+
+// 计算属性 - 各类型银行列表
+const cryptoWallets = computed(() => bankData.value[BankType.CRYPTO] || [])
+const unionpayCards = computed(() => bankData.value[BankType.UNIONPAY] || [])
+const wireTransfers = computed(() => bankData.value[BankType.WIRE_TRANSFER] || [])
+const creditCards = computed(() => bankData.value[BankType.CREDIT_CARD] || [])
+console.log(unionpayCards, bankData.value, 231232);
+
+
+
+// 弹出层ref
+const deletePopup = ref(null)
+const deleteTarget = ref(null)
+
+
+// 开始编辑
+const startEdit = (item) => {
+    editingId.value = item.id
+}
+
+// 取消编辑
+const cancelEdit = () => {
+    editingId.value = null
+    getBankInfo() // 重新加载数据
+}
+
+// 保存银行信息
+const saveBank = async (item) => {
+
+    if (item.approveStatus == 1) {
+        uni.showToast({
+            title: "加密钱包认证审核中",
+            icon: "none"
+        });
+        return;
+    }
+    try {
+        const params = { ...item }
+        params.defaultBank = params.defaultBank ? 1 : 0
+
+        if (params.type === BankType.CREDIT_CARD && params.expiryYearMonth) {
+            const [year, month] = params.expiryYearMonth.split('/')
+            params.expiryYear = year
+            params.expiryMonth = month
+        }
+        const res = await personalApi.customBankUpdate(params)
+        if (res.code === 200) {
+            uni.hideLoading()
+            uni.showToast({
+                title: t('Msg.Success'),
+                icon: 'success'
+            })
+            editingId.value = null
+            getBankInfo()
+        } else {
+            throw new Error(res.msg)
+        }
+    } catch (error) {
+        uni.hideLoading()
+        uni.showToast({
+            title: error.message || t('Msg.Fail'),
+            icon: 'none'
+        })
+    }
+}
+// 删除
+const confirmDelete = (item) => {
+    if (item.approveStatus == 1) {
+        uni.showToast({
+            title: '加密钱包认证审核中',
+            icon: 'none'
+        })
+        return
+    }
+    deleteTarget.value = item
+    deletePopup.value.open()
+}
+
+// 关闭删除弹窗
+const closeDeletePopup = () => {
+    deletePopup.value.close()
+    deleteTarget.value = null
+}
+
+// 执行删除
+const deleteBank = async () => {
+    if (!deleteTarget.value) return
+    uni.showLoading({ title: t('Btn.Deleting') })
+    try {
+        const res = await personalApi.customBankDelete({
+            ids: [deleteTarget.value.id]
+        })
+        if (res.code === 200) {
+            uni.hideLoading()
+            uni.showToast({
+                title: t('Msg.DeleteSuccess'),
+                icon: 'success'
+            })
+            deletePopup.value.close()
+            deleteTarget.value = null
+            getBankInfo()
+        } else {
+            throw new Error(res.msg)
+        }
+    } catch (error) {
+        uni.hideLoading()
+        uni.showToast({
+            title: error.message || t('Msg.Fail'),
+            icon: 'none'
+        })
+        deletePopup.value.close()
+    }
+}
+// 新增银行信息
+const addBankDialogRef = ref(null);
+function openAddBankCard() {
+    const wallets = cryptoWallets.value || [];
+
+    // 1️⃣ 没有钱包
+    if (wallets.length === 0) {
+        addBankDialogRef.value?.open();
+        return;
+    }
+
+    // 2️⃣ 是否存在未认证钱包
+    const hasUnAuth = wallets.some(
+        item => item.authStatus === 0 || item.approveStatus === 1
+    );
+
+    if (hasUnAuth) {
+        uni.showToast({
+            title: "加密钱包未认证",
+            icon: "none"
+        });
+        return;
+    }
+
+    // 3️⃣ 是否达到上限
+    if (wallets.length >= 2) {
+        uni.showToast({
+            title: t('blockchain.item9'),
+            icon: "none"
+        });
+        return;
+    }
+
+    // 4️⃣ 正常打开
+    addBankDialogRef.value?.open();
+}
+// 新增银行信息成功回调
+const addSuccess = (e) => {
+    openKycDialog(e)
+    getBankInfo();
+}
+
+// kyc认证和证件认证
+const kycDialogRef = ref(null);
+const cardDialogRef = ref(null);
+// kyc认证
+const doReady = (bankId, item) => {
+    if (item.approveStatus == 1) {
+        uni.showToast({
+            title: "加密钱包认证审核中",
+            icon: "none"
+        });
+        return;
+    }
+    if (item.authStatus == 1) {
+        uni.showToast({
+            title: "加密钱包已认证",
+            icon: "none"
+        });
+        return;
+    }
+    openKycDialog(bankId)
+}
+// 打开kyc认证弹窗
+function openKycDialog(e) {
+    kycDialogRef.value?.open(e);
+}
+// 打开证件认证弹窗
+function openCardDialog(e, item) {
+    if (item.approveStatus == 1) {
+        uni.showToast({
+            title: "加密钱包认证审核中",
+            icon: "none"
+        });
+        return;
+    }
+    if (item.authStatus == 1) {
+        uni.showToast({
+            title: "加密钱包已认证",
+            icon: "none"
+        });
+        return;
+    }
+    cardDialogRef.value?.open(e, item);
+}
+
+
+// 预览图片
+const previewImage = (path) => {
+    uni.previewImage({
+        urls: [updateUrl + path]
+    })
+}
+
+// 掩码卡号
+const maskCardNumber = (num) => {
+    if (!num) return '--'
+    if (num.length <= 4) return num
+    return '**** **** **** ' + num.slice(-4)
+}
+
+// 获取银行列表
+const getBankList = async () => {
+    const res = await personalApi.BankList({})
+    if (res.code === 200) {
+        bankOptions.value = res.data
+    }
+}
+
+// 获取银行信息
+const getBankInfo = async () => {
+    uni.showLoading({ title: t('Btn.Loading') })
+    try {
+        const res = await personalApi.customBankList({})
+        if (res.code === 200) {
+            // 清空数据
+            bankData.value = {
+                [BankType.CRYPTO]: [],
+                [BankType.UNIONPAY]: [],
+                [BankType.WIRE_TRANSFER]: [],
+                [BankType.CREDIT_CARD]: []
+            }
+            // 分类数据
+            res.data.forEach(item => {
+                item.defaultBank = !!item.defaultBank
+                if (item.type === BankType.UNIONPAY) {
+                    bankData.value[BankType.UNIONPAY].push(item)
+                } else if (item.type === BankType.WIRE_TRANSFER) {
+                    bankData.value[BankType.WIRE_TRANSFER].push(item)
+                } else if (item.type === BankType.CREDIT_CARD) {
+                    item.expiryYearMonth = item.expiryYear && item.expiryMonth
+                        ? `${item.expiryYear}/${item.expiryMonth}`
+                        : ''
+                    bankData.value[BankType.CREDIT_CARD].push(item)
+                } else if (item.type === BankType.CRYPTO) {
+                    bankData.value[BankType.CRYPTO].push(item)
+                }
+            })
+        }
+    } catch (error) {
+        uni.showToast({
+            title: error.message || t('Msg.Fail'),
+            icon: 'none'
+        })
+    } finally {
+        uni.hideLoading()
+    }
+}
+// 获取银行信息
+onMounted(() => {
+    getBankList()
+    getBankInfo();
+});
+</script>
+
+
+
+<style scoped lang="scss">
+.user-form {
+    margin-top: px2rpx(30);
+}
+
+:deep(.uni-row1) {
+    .uni-col {
+        padding: 0 10px !important;
+    }
+
+    .uni-forms-item {
+        min-height: px2rpx(79);
+        margin-bottom: px2rpx(10);
+    }
+
+    .uni-easyinput__content {
+        border: none !important;
+        background-color: var(--color-zinc-100) !important;
+    }
+}
+
+.bank-menu {
+    background: #fff;
+    overflow: hidden;
+
+    .bank-menu-item {
+        display: flex;
+        align-items: center;
+        gap: px2rpx(12);
+        padding: px2rpx(10) px2rpx(16);
+        cursor: pointer;
+        border: 1px solid #f3f4f6;
+        font-size: px2rpx(16);
+        font-weight: 500;
+        color: #1f2937;
+        transition: all 0.3s;
+        height: px2rpx(50);
+        border-radius: px2rpx(8);
+        margin-bottom: px2rpx(8);
+
+        .bank-icon {
+            width: px2rpx(50);
+        }
+
+        &.active {
+            background: #ea2027;
+            color: #fff;
+            border-radius: px2rpx(0);
+        }
+
+        &:hover {
+            background: #f9fafb;
+        }
+
+        &.active:hover {
+            background: #d11920;
+        }
+    }
+}
+
+.bank-content {
+    background: #fff;
+    border-radius: px2rpx(8);
+    padding: 0 px2rpx(24);
+
+    .bank-info {
+        .bank-header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            margin-bottom: px2rpx(24);
+            padding-bottom: px2rpx(16);
+            border-bottom: 1px solid #f3f4f6;
+
+            .bank-title {
+                display: flex;
+                align-items: center;
+                font-size: px2rpx(20);
+                font-weight: 600;
+                color: #1f2937;
+                margin-bottom: px2rpx(20);
+            }
+
+            .bank-actions {
+                display: flex;
+                gap: px2rpx(12);
+                align-items: center;
+                justify-content: flex-end;
+
+                .action-btn {
+                    padding: px2rpx(8) px2rpx(16);
+                    border: 1px solid #d1d5db;
+                    border-radius: px2rpx(4);
+                    font-size: px2rpx(14);
+                    cursor: pointer;
+                    background: #fff;
+                    transition: all 0.3s;
+
+                    &:hover {
+                        background: #f3f4f6;
+                    }
+
+                    &.delete {
+                        background: #ea2027;
+                        color: #fff;
+                        border-color: #ea2027;
+
+                        &:hover {
+                            background: #d11920;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    .photo-upload {
+        display: flex;
+        gap: px2rpx(16);
+
+        .photo-item {
+            width: px2rpx(120);
+            height: px2rpx(120);
+            border-radius: px2rpx(8);
+            overflow: hidden;
+            background: #f3f4f6;
+
+            .photo-preview {
+                width: 100%;
+                height: 100%;
+            }
+        }
+    }
+
+    .add-wallet-btn {
+        margin-top: px2rpx(24);
+        height: px2rpx(48);
+        background: #ea2027;
+        color: #fff;
+        border-radius: px2rpx(4);
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: px2rpx(8);
+        font-size: px2rpx(16);
+        font-weight: 600;
+        cursor: pointer;
+        transition: all 0.3s;
+
+        &:hover {
+            background: #d11920;
+        }
+    }
+}
+</style>

+ 562 - 0
pages/mine/components/CardAuthDialog.vue

@@ -0,0 +1,562 @@
+<template>
+    <uni-popup ref="cardRef" type="center" background-color="#fff">
+        <view class="dialog-container">
+            <view class="dialog-header">
+                <text class="dialog-title" v-t="'PersonalManagement.CardVerify.Title'" />
+                <view class="dialog-close" @click="close">
+                    <text>×</text>
+                </view>
+            </view>
+            <view class="qrcode-page">
+                <view class="container">
+                    <view class="title" v-t="'PersonalManagement.CardVerify.UploadTitle'"></view>
+                    <!-- 说明 -->
+                    <uni-file-picker limit="9" :title="t('PersonalManagement.CardVerify.UploadTip')" @select="fileSelect"
+                        @delete="fileDelete"></uni-file-picker>
+
+
+
+                    <!-- 邮箱验证码部分 -->
+                    <view>
+                        <view class="code-input-wrapper">
+                            <view class="code-input">
+                                <cwg-input v-model:value="cardEmailCode" :label="t('newSignup.item9')"
+                                    fkey="cardEmailCode" type="text" :required="true" rulesKey="cardEmailCode"
+                                    :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>
+                    <view class="notice">
+                        <view class="notice-title">
+                            <text class="icon">ⓘ</text>
+                            <text>{{ t('PersonalManagement.CardVerify.NoticeTitle') }}</text>
+                        </view>
+                        <view class="notice-list">
+                            <text v-for="(item, index) in noticeItems" :key="index" class="notice-item">
+                                {{ index + 1 }}. {{ item }}
+                            </text>
+                        </view>
+                    </view>
+
+                    <!-- 演示视频 -->
+                    <cwg-video-player :video-url="videoUrl"></cwg-video-player>
+                </view>
+            </view>
+            <view class="dialog-footer">
+                <view class="btn btn-cancel" @click="close">{{ t('Btn.Cancel') }}</view>
+                <view class="btn btn-confirm" @click="submit">{{ t('Btn.Confirm') }}</view>
+            </view>
+        </view>
+    </uni-popup>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, nextTick } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { customApi } from '@/service/custom';
+import { showToast } from "@/utils/toast";
+import config from '@/config';
+import { userToken } from '@/composables/config'
+import { useEmailCountdown } from '@/hooks/useEmailCountdown';
+const {
+    time,
+    text: getCodeString,
+    canSend,
+    start,
+    restore
+} = useEmailCountdown()
+const { t, locale } = useI18n()
+const cardEmailCode = ref('')
+const form = ref({
+    bankId: null,
+    email: ''
+})
+const emit = defineEmits(["success"]);
+// 发送邮箱验证码
+async function sendEmailCode() {
+    try {
+        if (!form.value.bankId) {
+            showToast(t("PersonalManagement.CardVerify.MissingBankId"));
+            return false;
+        }
+        const res = await customApi.customBankCardSendCode({
+            bankId: form.value.bankId
+        });
+
+        if (res.code === 200) {
+            if (res.data && (res.data.emailCode || res.data.code)) {
+                cardEmailCode.value = res.data.emailCode || res.data.code;
+            }
+            start()
+            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 (!canSend.value) return
+    await sendEmailCode();
+}
+// 视频URL
+const videoUrl = computed(() => {
+    const lang = locale.value || 'en'
+    const videos = {
+        cn: "https://player.vimeo.com/video/1152417528?badge=0&autopause=0&player_id=0&app_id=58479",
+        zhHant: "https://player.vimeo.com/video/1152417614?badge=0&autopause=0&player_id=0&app_id=58479",
+        en: "https://player.vimeo.com/video/1152417671?badge=0&autopause=0&player_id=0&app_id=58479",
+    }
+    return videos[lang] || videos.en
+})
+// uni-file-picker 相关
+const selectedFiles = ref([]) // 选择的文件列表
+const uploadedFiles = ref([]) // 上传成功的文件列表
+// 选择文件
+const fileSelect = async (e) => {
+    selectedFiles.value = e.tempFiles
+    for (const file of e.tempFiles) {
+        await uploadFile(file)
+    }
+}
+
+// 上传文件
+const uploadFile = (file) => {
+    return new Promise((resolve, reject) => {
+        const tempFile = file.path || file.url
+        uni.uploadFile({
+            url: config.Host80 + '/custom/bank/upload',
+            filePath: tempFile,
+            name: 'file',
+            header: {
+                'Access-Token': userToken.value
+            },
+            success: (uploadRes) => {
+                const data = JSON.parse(uploadRes.data)
+                console.log(data, 1111);
+
+                if (data.code === 200) {
+                    // 保存上传成功后的文件信息
+                    uploadedFiles.value.push(data.data)
+                    resolve(data)
+                } else {
+                    uni.showToast({
+                        title: data.msg || '上传失败',
+                        icon: 'none'
+                    })
+                    reject(new Error(data.msg))
+                }
+            },
+            fail: (err) => {
+                uni.showToast({
+                    title: '上传失败',
+                    icon: 'none'
+                })
+                reject(err)
+            }
+        })
+    })
+}
+
+// 删除文件
+const fileDelete = (e) => {
+    const index = uploadedFiles.value.findIndex(f => f.uuid === e.tempFile.uuid)
+    if (index !== -1) {
+        uploadedFiles.value.splice(index, 1)
+    }
+}
+
+// 说明项
+const noticeItems = computed(() => [
+    t('PersonalManagement.CardVerify.NoticeItem1'),
+    t('PersonalManagement.CardVerify.NoticeItem2'),
+    t('PersonalManagement.CardVerify.NoticeItem3'),
+    t('PersonalManagement.CardVerify.NoticeItem4'),
+    t('PersonalManagement.CardVerify.NoticeItem5'),
+    t('PersonalManagement.CardVerify.NoticeItem6'),
+    t('PersonalManagement.CardVerify.NoticeItem7')
+])
+
+const cardRef = ref(null)
+// 打开弹窗
+const open = async (e, item) => {
+    form.value = { ...item, bankId: item.id }
+    console.log(item, 2222);
+    await nextTick();
+    restore()
+    cardRef.value?.open();
+};
+
+// 关闭弹窗
+const close = () => {
+    cardRef.value?.close();
+};
+const submit = async () => {
+    if (!cardEmailCode.value) {
+        showToast(t("vaildate.emailCode.empty"));
+        return;
+    }
+    try {
+        const res = await customApi.customBankCardVerify({
+            bankId: form.value.bankId,
+            emailCode: cardEmailCode.value,
+            fileList: uploadedFiles.value
+        });
+        if (res.code === 200) {
+            showToast(res.msg);
+            close();
+            emit("success");
+        } else {
+            showToast(res.msg || t("common.error"));
+        }
+    } catch (error) {
+        showToast(t("common.error"));
+    }
+};
+// 暴露方法
+defineExpose({
+    open,
+    close
+});
+</script>
+
+<style lang="scss" scoped>
+.dialog-container {
+    width: 90vw;
+    max-width: px2rpx(800);
+    max-height: 85vh;
+    background: #fff;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+
+    .dialog-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: px2rpx(20) px2rpx(24);
+        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+        flex-shrink: 0;
+
+        .dialog-title {
+            font-size: px2rpx(20);
+            font-weight: 700;
+            color: #fff;
+            letter-spacing: 0.5px;
+        }
+
+        .dialog-close {
+            width: px2rpx(36);
+            height: px2rpx(36);
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: px2rpx(32);
+            color: rgba(255, 255, 255, 0.9);
+            cursor: pointer;
+            transition: all 0.3s;
+            border-radius: 50%;
+            background: rgba(255, 255, 255, 0.1);
+
+            &:hover {
+                background: rgba(255, 255, 255, 0.2);
+                transform: rotate(90deg);
+            }
+
+            &:active {
+                transform: rotate(90deg) scale(0.9);
+            }
+        }
+    }
+
+    .dialog-footer {
+        display: flex;
+        gap: px2rpx(12);
+        justify-content: flex-end;
+        padding: px2rpx(16);
+        border-top: 1px solid #f3f4f6;
+
+        .btn {
+            min-width: px2rpx(120);
+            padding: px2rpx(12) px2rpx(24);
+            border-radius: px2rpx(6);
+            font-size: px2rpx(14);
+            font-weight: 600;
+            border: none;
+            cursor: pointer;
+            text-align: center;
+            transition: all 0.3s;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+
+            &.btn-cancel {
+                background: #f3f4f6;
+                color: #6b7280;
+
+                &:hover {
+                    background: #e5e7eb;
+                }
+
+                &:active {
+                    background: #d1d5db;
+                }
+            }
+
+            &.btn-confirm {
+                background: #ea2027;
+                color: #fff;
+
+                &:hover {
+                    background: #d11920;
+                }
+
+                &:active {
+                    background: #c01819;
+                }
+            }
+        }
+    }
+}
+
+.qrcode-page {
+    flex: 1;
+    overflow-y: auto;
+    background: #f8f9fa;
+
+    .container {
+        padding: px2rpx(32);
+
+        .title {
+            font-size: px2rpx(18);
+            font-weight: 600;
+            color: #2c3e50;
+            text-align: center;
+            margin-bottom: px2rpx(24);
+            padding-bottom: px2rpx(16);
+            border-bottom: 2px solid #e9ecef;
+        }
+
+        .qrcode-wrapper {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            margin-bottom: px2rpx(32);
+            padding: px2rpx(24);
+            background: #fff;
+            border-radius: px2rpx(12);
+            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+
+            .qrcode {
+                width: px2rpx(240);
+                height: px2rpx(240);
+            }
+        }
+
+        .notice {
+            background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+            border-radius: px2rpx(12);
+            padding: px2rpx(24);
+            margin-bottom: px2rpx(32);
+            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+            border-left: 4px solid #667eea;
+
+            .notice-title {
+                display: flex;
+                align-items: center;
+                gap: px2rpx(8);
+                font-size: px2rpx(16);
+                font-weight: 700;
+                color: #2c3e50;
+                margin-bottom: px2rpx(16);
+
+                .icon {
+                    width: px2rpx(24);
+                    height: px2rpx(24);
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    background: #667eea;
+                    color: #fff;
+                    border-radius: 50%;
+                    font-size: px2rpx(16);
+                    font-weight: bold;
+                }
+            }
+
+            .notice-list {
+                .notice-item {
+                    display: block;
+                    font-size: px2rpx(14);
+                    color: #495057;
+                    line-height: 1.8;
+                    margin-bottom: px2rpx(10);
+                    padding-left: px2rpx(8);
+                    position: relative;
+
+                    &:last-child {
+                        margin-bottom: 0;
+                    }
+
+                    &::before {
+                        content: '';
+                        position: absolute;
+                        left: 0;
+                        top: px2rpx(10);
+                        width: px2rpx(4);
+                        height: px2rpx(4);
+                        background: #667eea;
+                        border-radius: 50%;
+                    }
+                }
+            }
+        }
+
+        .video-section {
+            background: #fff;
+            border-radius: px2rpx(12);
+            padding: px2rpx(24);
+            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+
+            .video-title {
+                font-size: px2rpx(16);
+                font-weight: 600;
+                color: #2c3e50;
+                margin-bottom: px2rpx(16);
+                display: flex;
+                align-items: center;
+                gap: px2rpx(8);
+
+                &::before {
+                    content: '';
+                    width: px2rpx(4);
+                    height: px2rpx(18);
+                    background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
+                    border-radius: px2rpx(2);
+                }
+            }
+
+            .video-wrapper {
+                width: 100%;
+                border-radius: px2rpx(8);
+                overflow: hidden;
+                background: #000;
+                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+
+                video {
+                    width: 100%;
+                    height: px2rpx(300);
+                    display: block;
+                }
+            }
+        }
+    }
+}
+
+// 验证码区域
+
+.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: var(--color-error);
+    z-index: 1;
+    margin-left: px2rpx(8);
+
+    .cwg-button {
+        margin: 0;
+    }
+
+    .cwg-button .u-button {
+        border-radius: px2rpx(8);
+        height: px2rpx(46) !important;
+    }
+}
+
+/* 移动端优化 */
+@media screen and (max-width: 768px) {
+    .dialog-container {
+        width: 95vw;
+        max-height: 90vh;
+    }
+
+    .qrcode-page .container {
+        padding: px2rpx(20);
+
+        .qrcode-wrapper .qrcode {
+            width: px2rpx(200);
+            height: px2rpx(200);
+        }
+
+        .video-section .video-wrapper video {
+            height: px2rpx(240);
+        }
+    }
+}
+
+/* 滚动条美化 */
+.qrcode-page::-webkit-scrollbar {
+    width: px2rpx(6);
+}
+
+.qrcode-page::-webkit-scrollbar-track {
+    background: #f1f1f1;
+}
+
+.qrcode-page::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: px2rpx(3);
+
+    &:hover {
+        background: #a8a8a8;
+    }
+}
+</style>

+ 234 - 0
pages/mine/components/FileManagementTab.vue

@@ -0,0 +1,234 @@
+<template>
+    <view>
+        <view class="content-title" v-if="current != 3">
+            <view v-t="'PersonalManagement.Title.FileManagement'"></view>
+            <view class="content-title-btns">
+                <view v-if="!isSHowBtn.isSHowIdentity" class="btn-primary" @click="openAddFileDialog(1)">
+                    <cwg-icon icon="crm-plus" :size="16" color="#fff" />
+                    <text v-t="'PersonalManagement.Title.ProofOfIdentity'" />
+                </view>
+                <view v-if="!isSHowBtn.isSHowAddress" class="btn-primary" @click="openAddFileDialog(2)">
+                    <cwg-icon icon="crm-plus" :size="16" color="#fff" />
+                    <text v-t="'PersonalManagement.Title.ProofOfAddress'" />
+                </view>
+                <view class="btn-primary" @click="openAddFileDialog(3)">
+                    <cwg-icon icon="crm-plus" :size="16" color="#fff" />
+                    <text v-t="'PersonalManagement.Title.AttachedFile'" />
+                </view>
+            </view>
+        </view>
+        <cwg-tabel ref="tableRef" :columns="columns" :api="customFileApi" :show-operation="false"
+            :showPagination="false">
+            <template #avatar="{ row }">
+                <image :src="row.avatar" class="avatar" mode="widthFix" />
+                <cwg-file :path="row.path" />
+            </template>
+
+            <template #type="{ row }">
+                <view :class="['status-badge', row.status]">{{ typeMap[row.type] }}</view>
+            </template>
+            <template #status="{ row }">
+                <view :class="['status-badge', row.status]">{{ stateMap[row.status] }}</view>
+            </template>
+            <template #btn="{ row }">
+                <text :class="['operation-btn', row.status !== 4 ? 'disabled' : '']" @click="openAddFile(row)">
+                    <cwg-icon name="crm-image" :size="16" color="#1d293d" />
+                    <text v-t="'State.Again'" />
+                </text>
+            </template>
+        </cwg-tabel>
+        <add-file-dialog ref="addFileDialog" @file-added="customFileList" @success="addSuccess" />
+    </view>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, onMounted } from 'vue';
+import { useI18n } from 'vue-i18n';
+import AddFileDialog from './AddFileDialog.vue';
+const { t } = useI18n();
+import { personalApi } from '@/service/personal';
+const stateMap = computed(() => ({
+    1: t('State.ToBeProcessed'),
+    2: t('State.Completed'),
+    3: t('State.Refused'),
+    4: t('State.Again')
+}));
+const typeMap = computed(() => ({
+    1: t('PersonalManagement.Title.ProofOfIdentity'),
+    2: t('PersonalManagement.Title.ProofOfIdentity'),
+    3: t('PersonalManagement.Title.ProofOfAddress'),
+    4: t('PersonalManagement.Title.ProofOfAddress'),
+    10: t('PersonalManagement.Title.AttachedFile')
+}));
+const btnMap = computed(() => ({
+    1: t('PersonalManagement.Title.ProofOfIdentity'),
+    2: t('PersonalManagement.Title.ProofOfAddress'),
+    3: t('PersonalManagement.Title.AttachedFile')
+}));
+interface Props {
+    icon: string;
+    label: string;
+    value: string;
+    isLast?: boolean;
+}
+const tableRef = ref<any>(null);
+const tableData = computed(() => {
+    return tableRef.value ? tableRef.value.tableData : [];
+});
+const isSHowBtn = computed(() => {
+    const a = tableData.value
+    let tableIdentity = [];
+    const tableAddress = [];
+    const tableAdditional = [];
+    a.forEach((item) => {
+        if (item.type == 1 || item.type == 2) {
+            tableIdentity.push(item);
+        } else if (item.type == 3) {
+            tableAddress.push(item);
+        } else {
+            tableAdditional.push(item);
+        }
+    });
+    const isSHowIdentity = tableIdentity.length > 1;
+    const isSHowAddress = tableAddress.length > 0;
+    return { isSHowIdentity, isSHowAddress };
+});
+
+// 表格列配置
+const columns = ref([
+    {
+        prop: 'id',
+        label: '#',
+        align: 'left'
+    },
+    {
+        prop: 'path',
+        label: 'Document',
+        type: 'file',
+        align: 'left'
+    },
+    {
+        prop: 'type',
+        label: 'File Name/Type',
+        align: 'left',
+        slot: 'type'
+    },
+    {
+        prop: 'uploadTime',
+        label: 'Upload Date/Time',
+        type: 'date',
+        dateFormat: 'YYYY-MM-DD HH:mm',
+        align: 'left'
+    },
+    {
+        prop: 'status',
+        label: 'Status',
+        type: 'tag',
+        tagMap: { 1: '启用', 10: '禁用' },
+        tagTypeMap: { 1: 'success', 0: 'danger' },
+        slot: 'status',
+        align: 'left'
+    },
+    {
+        prop: 'status',
+        label: '操作',
+        slot: 'btn',
+        align: 'left'
+    }
+])
+
+const addFileDialog = ref(null);
+const customFileApi = ref(null)
+customFileApi.value = personalApi.customFileList
+const openAddFileDialog = (type) => {
+    addFileDialog.value.open({ type, title: btnMap.value[type], tableData: tableData.value });
+}
+const openAddFile = (row) => {
+    if (row.status != 4) {
+        return
+    }
+    let type
+    switch (row.type) {
+        case 1:
+            type = 1
+            break;
+        case 2:
+            type = 1
+            break;
+        case 3:
+            type = 2
+            break;
+        case 10:
+            type = 3
+            break;
+    }
+    addFileDialog.value.open({ type, title: btnMap.value[type], tableData: tableData.value, currentFile: row });
+}
+const addSuccess = () => {
+    tableRef.value.refreshTable()
+}
+defineProps<Props>();
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.avatar {
+    width: px2rpx(60);
+    height: px2rpx(60);
+    border-radius: 4px;
+}
+
+.content-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-size: px2rpx(20);
+    font-weight: 500;
+
+    .content-title-btns {
+        margin: px2rpx(8) 0;
+
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: px2rpx(12);
+
+        .btn-primary {
+            min-width: px2rpx(120);
+            background-color: var(--color-error);
+            color: white;
+            padding: 0 px2rpx(12);
+            border: none;
+            font-size: px2rpx(14);
+            text-align: center;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            gap: px2rpx(8);
+        }
+
+        .btn-primary:active {
+            background-color: var(--color-navy-700);
+        }
+    }
+}
+
+.operation-btn {
+    :deep(span) {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: px2rpx(4);
+        cursor: pointer;
+        background-color: var(--color-slate-150);
+        padding: px2rpx(8) 0;
+    }
+}
+
+.operation-btn.disabled {
+    cursor: not-allowed;
+    opacity: 0.5;
+}
+</style>

+ 0 - 62
pages/mine/components/InfoRow.vue

@@ -1,62 +0,0 @@
-<template>
-    <view :class="['info-row', { 'info-row-last': isLast }]">
-        <view class="info-icon">
-            <cwg-icon :name="icon" :size="20" color="#9ca3af" />
-        </view>
-        <view class="info-content">
-            <text class="info-label">{{ label }}</text>
-            <text class="info-value">{{ value || '-' }}</text>
-        </view>
-    </view>
-</template>
-
-<script setup lang="ts">
-interface Props {
-    icon: string;
-    label: string;
-    value: string;
-    isLast?: boolean;
-}
-
-defineProps<Props>();
-</script>
-
-<style scoped lang="scss">
-@import "@/uni.scss";
-
-.info-row {
-    display: flex;
-    align-items: center;
-    gap: px2rpx(12);
-    padding: px2rpx(12) 0;
-    border-bottom: 1px solid #f9fafb;
-}
-
-.info-row-last {
-    border-bottom: none;
-}
-
-.info-icon {
-    flex-shrink: 0;
-    width: px2rpx(24);
-    height: px2rpx(24);
-}
-
-.info-content {
-    flex: 1;
-    min-width: 0;
-    display: flex;
-    flex-direction: column;
-}
-
-.info-label {
-    font-size: px2rpx(12);
-    color: #6b7280;
-    margin-bottom: px2rpx(4);
-}
-
-.info-value {
-    font-size: px2rpx(14);
-    color: #111827;
-}
-</style>

+ 377 - 0
pages/mine/components/KycAuthDialog.vue

@@ -0,0 +1,377 @@
+<template>
+    <uni-popup ref="kycRef" type="center" background-color="#fff">
+        <view class="dialog-container">
+            <view class="dialog-header">
+                <text class="dialog-title">{{ t('blockchain.item2') }}</text>
+                <view class="dialog-close" @click="close">
+                    <text>×</text>
+                </view>
+            </view>
+            <view class="qrcode-page">
+                <view class="container">
+                    <view class="title">{{ t('ApplicationDialog.Des11') }}</view>
+                    <!-- 二维码 -->
+                    <view class="qrcode-wrapper">
+                        <QrCode v-if="qrCodeUrl" :text="qrCodeUrl"></QrCode>
+                    </view>
+                    <!-- 说明 -->
+                    <view class="notice">
+                        <view class="notice-title">
+                            <text class="icon">ⓘ</text>
+                            <text>{{ t('PersonalManagement.KYCVerify.NoticeTitle') }}</text>
+                        </view>
+                        <view class="notice-list">
+                            <text v-for="(item, index) in noticeItems" :key="index" class="notice-item">
+                                {{ index + 1 }}. {{ item }}
+                            </text>
+                        </view>
+                    </view>
+
+                    <!-- 演示视频 -->
+                    <cwg-video-player :video-url="videoUrl"></cwg-video-player>
+                </view>
+            </view>
+        </view>
+    </uni-popup>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, nextTick } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { customApi } from '@/service/custom';
+import QrCode from "@/components/QrCode.vue";
+const { t, locale } = useI18n()
+// 二维码链接
+const qrCodeUrl = ref("");
+// API 响应码
+const responseCode = ref(200);
+// 设备元信息
+const metaInfo = ref<Record<string, any> | null>(null);
+
+/**
+ * 获取设备元信息
+ */
+function getMetaInfo() {
+    try {
+        // 安全获取 metaInfo,兼容不同环境
+        if (typeof window !== "undefined" && (window as any).getMetaInfo) {
+            metaInfo.value = (window as any).getMetaInfo();
+            metaInfo.value = { ...metaInfo.value, deviceType: "h5" };
+        } else {
+            // 默认值
+            metaInfo.value = { deviceType: "h5" };
+        }
+    } catch (error) {
+        //  console.warn("获取设备信息失败:", error);
+        metaInfo.value = { deviceType: "h5" };
+    }
+}
+
+/**
+ * 获取 WebSDK 链接
+ * @param cardId 卡片ID
+ */
+async function getWebsdkLink(bankId) {
+    if (!bankId) {
+        console.warn("bankId 为空,无法获取 WebSDK 链接");
+        return;
+    }
+    try {
+        // 获取设备信息
+        getMetaInfo();
+        // 调用 API
+        const res = await customApi.getWebsdkLink2({
+            bankId,
+            metaInfo: metaInfo.value,
+        });
+
+        responseCode.value = res.code || 201;
+        if (res.code === 200 && res.data) {
+            try {
+                // 安全解析 JSON
+                const data = typeof res.data === "string" ? JSON.parse(res.data) : res.data;
+                qrCodeUrl.value = data.url || data.link || "";
+            } catch (parseError) {
+                console.error("解析响应数据失败:", parseError);
+                responseCode.value = 201;
+            }
+        } else {
+            qrCodeUrl.value = "";
+        }
+    } catch (error: any) {
+        console.error("获取 WebSDK 链接失败:", error);
+        responseCode.value = 201;
+        qrCodeUrl.value = "";
+    }
+}
+// 视频URL
+const videoUrl = computed(() => {
+    const lang = locale.value || 'en'
+    const videos = {
+        cn: "https://player.vimeo.com/video/1153328212?badge=0&autopause=0&player_id=0&app_id=58479",
+        zh: "https://player.vimeo.com/video/1153328237?badge=0&autopause=0&player_id=0&app_id=58479",
+        en: "https://player.vimeo.com/video/1153328184?badge=0&autopause=0&player_id=0&app_id=58479"
+    }
+    return videos[lang] || videos.en
+})
+
+// 说明项
+const noticeItems = computed(() => [
+    t('PersonalManagement.KYCVerify.NoticeItem1'),
+    t('PersonalManagement.KYCVerify.NoticeItem2'),
+    t('PersonalManagement.KYCVerify.NoticeItem3'),
+    t('PersonalManagement.KYCVerify.NoticeItem4'),
+    t('PersonalManagement.KYCVerify.NoticeItem5'),
+    t('PersonalManagement.KYCVerify.NoticeItem6')
+])
+
+// 视频错误处理
+const onVideoError = (e) => {
+    console.error('视频加载失败', e)
+    uni.showToast({
+        title: '视频加载失败',
+        icon: 'none'
+    })
+}
+const kycRef = ref(null)
+// 打开弹窗
+const open = async (e) => {
+    await nextTick();
+    getWebsdkLink(e)
+    kycRef.value?.open();
+};
+
+// 关闭弹窗
+const close = () => {
+    kycRef.value?.close();
+};
+// 暴露方法
+defineExpose({
+    open,
+    close
+});
+</script>
+
+<style lang="scss" scoped>
+.dialog-container {
+    width: 90vw;
+    max-width: px2rpx(800);
+    max-height: 85vh;
+    background: #fff;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+
+    .dialog-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: px2rpx(20) px2rpx(24);
+        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+        flex-shrink: 0;
+
+        .dialog-title {
+            font-size: px2rpx(20);
+            font-weight: 700;
+            color: #fff;
+            letter-spacing: 0.5px;
+        }
+
+        .dialog-close {
+            width: px2rpx(36);
+            height: px2rpx(36);
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: px2rpx(32);
+            color: rgba(255, 255, 255, 0.9);
+            cursor: pointer;
+            transition: all 0.3s;
+            border-radius: 50%;
+            background: rgba(255, 255, 255, 0.1);
+
+            &:hover {
+                background: rgba(255, 255, 255, 0.2);
+                transform: rotate(90deg);
+            }
+
+            &:active {
+                transform: rotate(90deg) scale(0.9);
+            }
+        }
+    }
+}
+
+.qrcode-page {
+    flex: 1;
+    overflow-y: auto;
+    background: #f8f9fa;
+
+    .container {
+        padding: px2rpx(32);
+
+        .title {
+            font-size: px2rpx(18);
+            font-weight: 600;
+            color: #2c3e50;
+            text-align: center;
+            margin-bottom: px2rpx(24);
+            padding-bottom: px2rpx(16);
+            border-bottom: 2px solid #e9ecef;
+        }
+
+        .qrcode-wrapper {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            margin-bottom: px2rpx(32);
+            padding: px2rpx(24);
+            background: #fff;
+            border-radius: px2rpx(12);
+            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+
+            .qrcode {
+                width: px2rpx(240);
+                height: px2rpx(240);
+            }
+        }
+
+        .notice {
+            background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+            border-radius: px2rpx(12);
+            padding: px2rpx(24);
+            margin-bottom: px2rpx(32);
+            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+            border-left: 4px solid #667eea;
+
+            .notice-title {
+                display: flex;
+                align-items: center;
+                gap: px2rpx(8);
+                font-size: px2rpx(16);
+                font-weight: 700;
+                color: #2c3e50;
+                margin-bottom: px2rpx(16);
+
+                .icon {
+                    width: px2rpx(24);
+                    height: px2rpx(24);
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    background: #667eea;
+                    color: #fff;
+                    border-radius: 50%;
+                    font-size: px2rpx(16);
+                    font-weight: bold;
+                }
+            }
+
+            .notice-list {
+                .notice-item {
+                    display: block;
+                    font-size: px2rpx(14);
+                    color: #495057;
+                    line-height: 1.8;
+                    margin-bottom: px2rpx(10);
+                    padding-left: px2rpx(8);
+                    position: relative;
+
+                    &:last-child {
+                        margin-bottom: 0;
+                    }
+
+                    &::before {
+                        content: '';
+                        position: absolute;
+                        left: 0;
+                        top: px2rpx(10);
+                        width: px2rpx(4);
+                        height: px2rpx(4);
+                        background: #667eea;
+                        border-radius: 50%;
+                    }
+                }
+            }
+        }
+
+        .video-section {
+            background: #fff;
+            border-radius: px2rpx(12);
+            padding: px2rpx(24);
+            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+
+            .video-title {
+                font-size: px2rpx(16);
+                font-weight: 600;
+                color: #2c3e50;
+                margin-bottom: px2rpx(16);
+                display: flex;
+                align-items: center;
+                gap: px2rpx(8);
+
+                &::before {
+                    content: '';
+                    width: px2rpx(4);
+                    height: px2rpx(18);
+                    background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
+                    border-radius: px2rpx(2);
+                }
+            }
+
+            .video-wrapper {
+                width: 100%;
+                border-radius: px2rpx(8);
+                overflow: hidden;
+                background: #000;
+                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+
+                video {
+                    width: 100%;
+                    height: px2rpx(300);
+                    display: block;
+                }
+            }
+        }
+    }
+}
+
+/* 移动端优化 */
+@media screen and (max-width: 768px) {
+    .dialog-container {
+        width: 95vw;
+        max-height: 90vh;
+    }
+
+    .qrcode-page .container {
+        padding: px2rpx(20);
+
+        .qrcode-wrapper .qrcode {
+            width: px2rpx(200);
+            height: px2rpx(200);
+        }
+
+        .video-section .video-wrapper video {
+            height: px2rpx(240);
+        }
+    }
+}
+
+/* 滚动条美化 */
+.qrcode-page::-webkit-scrollbar {
+    width: px2rpx(6);
+}
+
+.qrcode-page::-webkit-scrollbar-track {
+    background: #f1f1f1;
+}
+
+.qrcode-page::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: px2rpx(3);
+
+    &:hover {
+        background: #a8a8a8;
+    }
+}
+</style>

+ 580 - 0
pages/mine/components/PersonalInfoTab.vue

@@ -0,0 +1,580 @@
+<template>
+    <view class="personal-info-tab">
+        <view class="user-form">
+            <uni-row class="demo-uni-row">
+                <uni-col :xs="24" :sm="24" :md="24" :lg="6" :xl="6">
+                    <view class="avatar-section">
+                        <cwg-input v-model:value="formData.idBackUrl" type="upload" fkey="idBackUrl"
+                            rulesKey="idBackUrl" :is-upload-d="true" accept="image/png, image/jpeg, image/jpg"
+                            :readonly="isReadonly" :disabled="isReadonly" @change="handleChange">
+                            <view class="cwg-upload">
+                            </view>
+                        </cwg-input>
+                        <view class="text name">{{ formData.firstName }} {{ formData.lastName }}</view>
+                        <view class="text cid">CID:{{ formData.cId }}</view>
+                        <view class="btn-primary" @click="handleEditProfile">
+                            <cwg-icon name="crm-photo-film" :size="16" color="#fff" />
+                            上传头像
+                        </view>
+                    </view>
+                </uni-col>
+                <uni-col :xs="24" :sm="24" :md="24" :lg="18" :xl="18">
+                    <uni-forms ref="baseForm" :model="formData" labelWidth="200" label-position="top" :rules="rules"
+                        class="base-info-form">
+                        <uni-row class="demo-uni-row uni-row1">
+                            <!-- 客户类型 -->
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8">
+                                <uni-forms-item :label="t('ImproveImmediately.Label.CustomerType')">
+                                    <cwg-combox :clearable="false" v-model:value="formData.customType"
+                                        :options="customTypeOptions" :placeholder="t('placeholder.choose')" />
+                                </uni-forms-item>
+                            </uni-col>
+                            <!-- 公司名称 -->
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8" v-if="formData.companyName">
+                                <uni-forms-item :label="t('ImproveImmediately.Label.CompanyName')">
+                                    <uni-easyinput :clearable="false" v-model="formData.companyName"
+                                        :placeholder="t('placeholder.input')" />
+                                </uni-forms-item>
+                            </uni-col>
+
+                            <!-- 姓 -->
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8">
+                                <uni-forms-item :label="formData.customType == 2
+                                    ? t('ImproveImmediately.Label.CorporationLastName')
+                                    : t('ImproveImmediately.Label.LastName')">
+                                    <uni-easyinput :clearable="false" v-model="formData.lastName"
+                                        :placeholder="t('placeholder.input')" />
+                                </uni-forms-item>
+                            </uni-col>
+                            <!-- 名 -->
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8">
+                                <uni-forms-item :label="formData.customType == 2
+                                    ? t('ImproveImmediately.Label.CorporationName')
+                                    : t('ImproveImmediately.Label.Name')">
+                                    <uni-easyinput :clearable="false" v-model="formData.firstName"
+                                        :placeholder="t('placeholder.input')" />
+                                </uni-forms-item>
+                            </uni-col>
+                            <!-- 法人中间名 -->
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8" v-if="locale == 'en'">
+                                <uni-forms-item :label="formData.customType == 2
+                                    ? t('ImproveImmediately.Label.CorporationMName')
+                                    : t('placeholder.middle')">
+                                    <uni-easyinput :clearable="false" v-model="formData.middle"
+                                        :placeholder="t('placeholder.input')" />
+                                </uni-forms-item>
+                            </uni-col>
+
+                            <!-- 国家 -->
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8" v-if="countryOptions.length">
+                                <uni-forms-item :label="t('ImproveImmediately.Label.Nationality')">
+                                    <cwg-combox :clearable="false" :filterable="true"
+                                        v-model:value="formData.nationality" :options="countryOptions"
+                                        :placeholder="t('placeholder.choose')" @change="changeCountry" />
+                                </uni-forms-item>
+                            </uni-col>
+                            <!-- 证件类型 -->
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8">
+                                <uni-forms-item :label="t('ImproveImmediately.Label.CardType')">
+                                    <cwg-combox :clearable="false" v-model:value="formData.customType"
+                                        :options="idTypeOptions" :placeholder="t('placeholder.choose')" />
+                                </uni-forms-item>
+                            </uni-col>
+                            <!-- 证件号 -->
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8">
+                                <uni-forms-item :label="t('ImproveImmediately.Label.IdentityID')">
+                                    <uni-easyinput :clearable="false" v-model="formData.identity"
+                                        :placeholder="t('placeholder.input')" />
+                                </uni-forms-item>
+                            </uni-col>
+                            <!-- 拼音 -->
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8" v-if="isZh">
+                                <uni-forms-item :label="t('ImproveImmediately.Label.NamePinYin')">
+                                    <uni-easyinput :clearable="false" v-model="formData.nameEn"
+                                        :placeholder="t('placeholder.input')" />
+                                </uni-forms-item>
+                            </uni-col>
+                            <!-- 性别 -->
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8">
+                                <uni-forms-item :label="t('ImproveImmediately.Label.Gender')">
+                                    <cwg-combox :clearable="false" v-model:value="formData.gender" :options="sexOptions"
+                                        :placeholder="t('placeholder.choose')" />
+                                </uni-forms-item>
+                            </uni-col>
+                            <!-- 生日 -->
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8">
+                                <uni-forms-item :label="t('ImproveImmediately.Label.Birthday')">
+                                    <uni-datetime-picker :clear-icon="false" type="datetime" return-type="timestamp"
+                                        v-model="formData.birth" :placeholder="t('placeholder.choose')" />
+                                </uni-forms-item>
+                            </uni-col>
+
+                            <!-- 国家/地区 -->
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8" v-if="countryOptions.length">
+                                <uni-forms-item :label="t('ImproveImmediately.Label.CountryRegionOfResidence')">
+                                    <cwg-combox :clearable="false" :filterable="true" v-model:value="formData.country"
+                                        :options="countryOptions" :placeholder="t('placeholder.choose')" />
+                                </uni-forms-item>
+                            </uni-col>
+                            <!-- 省份/州 -->
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8"
+                                v-if="stateOptions.length && isCountryCN">
+                                <uni-forms-item :label="t('ImproveImmediately.Label.ProvinceRegion')">
+                                    <cwg-combox :clearable="false" :filterable="true" v-model:value="formData.state"
+                                        :options="stateOptions" :placeholder="t('placeholder.choose')"
+                                        @change="onStateChange" />
+                                </uni-forms-item>
+                            </uni-col>
+                            <!-- 国外省份/州 -->
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8" v-if="!isCountryCN">
+                                <uni-forms-item :label="t('ImproveImmediately.Label.ProvinceRegion')">
+                                    <uni-easyinput :clearable="false" v-model="formData.state"
+                                        :placeholder="t('placeholder.input')" />
+                                </uni-forms-item>
+                            </uni-col>
+                            <!-- 城市 -->
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8"
+                                v-if="cityOptions.length && isCountryCN">
+                                <uni-forms-item :label="t('ImproveImmediately.Label.City')">
+                                    <cwg-combox :clearable="false" :filterable="true" v-model:value="formData.city"
+                                        :options="cityOptions" :placeholder="t('placeholder.choose')" />
+                                </uni-forms-item>
+                            </uni-col>
+                            <!-- 国外城市 -->
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8" v-if="!isCountryCN">
+                                <uni-forms-item :label="t('ImproveImmediately.Label.City')">
+                                    <uni-easyinput :clearable="false" v-model="formData.city"
+                                        :placeholder="t('placeholder.input')" />
+                                </uni-forms-item>
+                            </uni-col>
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8">
+                                <uni-forms-item :label="t('ImproveImmediately.Label.DetailedAddress')">
+                                    <uni-easyinput :clearable="false" v-model="formData.addressLines1"
+                                        :placeholder="t('placeholder.input')" />
+                                </uni-forms-item>
+                            </uni-col>
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8">
+                                <uni-forms-item :label="t('ImproveImmediately.Label.ZipCode')">
+                                    <uni-easyinput :clearable="false" v-model="formData.zipCode"
+                                        :placeholder="t('placeholder.input')" />
+                                </uni-forms-item>
+                            </uni-col>
+                            <uni-col :xs="24" :sm="24" :md="12" :lg="8" :xl="8">
+                                <uni-forms-item :label="t('ImproveImmediately.Label.DetailedAddressStandby')">
+                                    <uni-easyinput :clearable="false" v-model="formData.addressLines2"
+                                        :placeholder="t('placeholder.input')" />
+                                </uni-forms-item>
+                            </uni-col>
+
+                        </uni-row>
+                    </uni-forms>
+                </uni-col>
+            </uni-row>
+        </view>
+        <view class="btns">
+            <view class="btn-primary" @click="handleCancel">{{ t('Btn.Cancel') }}</view>
+            <view class="btn-primary" @click="handleNext">{{ t('Btn.Next') }}</view>
+        </view>
+    </view>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { personalApi } from '@/service/personal';
+import { Patterns, Validators } from '@/utils/validators';
+
+const { t, locale } = useI18n();
+
+interface PersonalInfo {
+    merchantOrderNo?: string;
+    cardTypeId?: string;
+    areaCode?: string;
+    mobile?: string;
+    email?: string;
+    firstName?: string;
+    lastName?: string;
+    middle?: string;
+    birthday?: string;
+    nationality?: string;
+    country?: string;
+    state?: string;
+    city?: string;
+    town?: string;
+    address?: string;
+    postCode?: string;
+    gender?: number;
+    occupation?: string;
+    annualSalary?: string;
+    accountPurpose?: string;
+    expectedMonthlyVolume?: string;
+    idType?: number;
+    idNumber?: string;
+    identity?: string;
+    ssn?: string;
+    issueDate?: string;
+    idNoExpiryDate?: string;
+    idFrontUrl?: string;
+    idBackUrl?: string;
+    idHoldUrl?: string;
+    ipAddress?: string;
+    cId?: string;
+    customId?: string;
+    customType?: number;
+    companyName?: string;
+    nameEn?: string;
+    birth?: string;
+    addressLines1?: string;
+    addressLines2?: string;
+    zipCode?: string;
+    photoStatus?: string;
+    kycStatus?: string;
+}
+
+const props = defineProps<{
+    modelValue: PersonalInfo;
+    isReadonly?: boolean;
+}>();
+
+const emit = defineEmits<{
+    'update:modelValue': [value: PersonalInfo];
+    'cancel': [];
+    'next': [];
+}>();
+
+const formData = computed({
+    get: () => props.modelValue,
+    set: (val) => emit('update:modelValue', val)
+});
+
+const baseForm = ref<any>(null);
+
+const sexOptions = ref([
+    { text: t('PersonalManagement.Label.Men'), value: 1 },
+    { text: t('PersonalManagement.Label.Women'), value: 2 }
+]);
+
+const customTypeOptions = ref([
+    { text: t('ImproveImmediately.Label.CustomerType1'), value: 1 },
+    { text: t('ImproveImmediately.Label.CustomerType2'), value: 2 }
+]);
+
+const idTypeOptions = ref([
+    { text: t('ImproveImmediately.Label.IDCard'), value: 2 },
+    { text: t('ImproveImmediately.Label.Passport'), value: 3 }
+]);
+
+// 验证函数
+function validateName(a: any, b?: any, c?: any) {
+    const reg = /^[A-Z\s]+$/i;
+    if (typeof c === 'function') {
+        const value = b;
+        const callback = c;
+        const val = String(value ?? '').trim();
+        if (!val) return callback(new Error(t('card.vaildate.v4')));
+        if (!reg.test(val)) return callback(new Error(t('card.vaildate.v38')));
+        if (val.length < 2 || val.length > 23) return callback(new Error(t('card.vaildate.v39')));
+        const firstName = String(formData.value?.firstName ?? '').trim();
+        const lastName = String(formData.value?.lastName ?? '').trim();
+        if (`${firstName} ${lastName}`.length > 23) return callback(new Error(t('card.vaildate.v40')));
+        return callback();
+    }
+    const val = String(a ?? '').trim();
+    if (!val) return t('card.vaildate.v4');
+    if (!reg.test(val)) return t('card.vaildate.v38');
+    if (val.length < 2 || val.length > 23) return t('card.vaildate.v39');
+    const firstName = String(formData.value?.firstName ?? '').trim();
+    const lastName = String(formData.value?.lastName ?? '').trim();
+    if (`${firstName} ${lastName}`.length > 23) return t('card.vaildate.v40');
+    return true;
+}
+
+function validateBirthday(a: any, b?: any, c?: any) {
+    if (typeof c === 'function') {
+        const value = b;
+        const callback = c;
+        const val = value;
+        if (!val) return callback(new Error(t('card.vaildate.v5')));
+        const today = new Date();
+        const birthDate = new Date(val);
+        let age = today.getFullYear() - birthDate.getFullYear();
+        const month = today.getMonth() - birthDate.getMonth();
+        if (month < 0 || (month === 0 && today.getDate() < birthDate.getDate())) age--;
+        if (age < 18) return callback(new Error(t('card.New.n3')));
+        return callback();
+    }
+    const val = a;
+    if (!val) return t('card.vaildate.v5');
+    const today = new Date();
+    const birthDate = new Date(val);
+    let age = today.getFullYear() - birthDate.getFullYear();
+    const month = today.getMonth() - birthDate.getMonth();
+    if (month < 0 || (month === 0 && today.getDate() < birthDate.getDate())) age--;
+    return age < 18 ? t('card.New.n3') : true;
+}
+
+function validateAddress(a: any, b?: any, c?: any) {
+    if (typeof c === 'function') {
+        const value = b;
+        const callback = c;
+        const val = String(value ?? '').trim();
+        if (!val) return callback(new Error(t('card.vaildate.v27')));
+        if (val.length < 2 || val.length > 40) return callback(new Error(t('card.New.n1')));
+        if (!Patterns.address.test(val)) return callback(new Error(t('card.New.n1')));
+        return callback();
+    }
+    const val = String(a ?? '').trim();
+    if (!val) return t('card.vaildate.v27');
+    if (val.length < 2 || val.length > 40) return t('card.New.n1');
+    return Patterns.address.test(val) ? true : t('card.New.n1');
+}
+
+const rules = {
+    areaCode: [Validators.required(t('card.vaildate.v1'))],
+    mobile: [
+        Validators.required(t('card.vaildate.v2')),
+        Validators.pattern(Patterns.mobile, t('card.vaildate.v44'))
+    ],
+    email: [
+        Validators.required(t('card.vaildate.v28')),
+        Validators.pattern(Patterns.email, t('card.vaildate.v28'))
+    ],
+    firstName: [Validators.required(t('card.vaildate.v3')), Validators.custom(validateName)],
+    lastName: [Validators.required(t('card.vaildate.v4')), Validators.custom(validateName)],
+    birthday: [
+        Validators.required(t('card.vaildate.v5'), 'change'),
+        Validators.custom(validateBirthday, 'change')
+    ],
+    nationality: [Validators.required(t('card.vaildate.v6'), 'change')],
+    town: [Validators.required(t('card.vaildate.v7'), 'change')],
+    address: [Validators.required(t('card.vaildate.v27')), Validators.custom(validateAddress)],
+    gender: [Validators.required(t('card.vaildate.v9'), 'change')],
+    postCode: [
+        Validators.required(t('card.vaildate.v8')),
+        Validators.pattern(Patterns.postcode, t('card.New.n2'))
+    ]
+};
+
+// 国家省份城市数据
+const countryList = ref<Array<any>>([]);
+const stateList = ref<Array<any>>([]);
+const cityList = ref<Array<any>>([]);
+const phoneCodes = ref<Array<{ text: string; value: string }>>([]);
+
+const isZh = computed(() => ['cn', 'zh', 'zhHant'].includes(locale.value));
+const isCountryCN = computed(() => ['CN', 'CNX', 'CNA', 'CNT'].includes(formData.value.country || ''));
+
+const getLangName = (item: any) => (isZh.value ? item.name : item.enName);
+const createOptions = (list: any[], valueKey = 'code') => {
+    return list.map((item) => ({
+        text: getLangName(item),
+        value: valueKey === 'name' ? getLangName(item) : item[valueKey],
+        id: item.id
+    }));
+};
+
+const countryOptions = computed(() => createOptions(countryList.value, 'code'));
+const stateOptions = computed(() => createOptions(stateList.value, 'name'));
+const cityOptions = computed(() => createOptions(cityList.value, 'name'));
+
+const getCountry = async () => {
+    const res = await personalApi.Country({});
+    if (res.code === 200) {
+        countryList.value = res.data;
+        phoneCodes.value = res.data.map((item: any) => ({
+            text: `+ ${item.callingCode}`,
+            value: item.callingCode
+        }));
+        if (formData.value.country) {
+            const country = countryList.value.find((i) => i.code === formData.value.country);
+            if (country) {
+                await getState(country.id);
+            }
+        }
+    }
+};
+
+const fetchRegion = async (pid?: number) => {
+    const res = await personalApi.Country({ pid });
+    if (res.code !== 200) {
+        return [];
+    }
+    return res.data || [];
+};
+
+const getState = async (pid: number) => {
+    stateList.value = await fetchRegion(pid);
+    if (formData.value.state) {
+        const item = stateList.value.find(
+            (i) => i.enName === formData.value.state || i.name === formData.value.state
+        );
+        if (item) {
+            await getCity(item.id);
+        }
+    }
+};
+
+const getCity = async (pid: number) => {
+    cityList.value = await fetchRegion(pid);
+};
+
+const changeCountry = async (val: any) => {
+    formData.value.state = '';
+    formData.value.city = '';
+    const item = countryOptions.value.find((i: any) => i.value === val);
+    if (item) {
+        await getState(item.id);
+    }
+};
+
+const onStateChange = async (val: any) => {
+    formData.value.city = '';
+    const item = stateOptions.value.find((i: any) => i.value === val);
+    if (item) {
+        await getCity(item.id);
+    }
+};
+
+function handleChange(value: any) {
+    emit('update:modelValue', { ...formData.value, [value.key]: value.value });
+}
+
+const handleEditProfile = () => {
+    // 头像上传逻辑
+};
+
+const handleCancel = () => {
+    emit('cancel');
+};
+
+const handleNext = () => {
+    emit('next');
+};
+
+onMounted(() => {
+    getCountry();
+});
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.personal-info-tab {
+    .user-form {
+        margin-top: px2rpx(30);
+
+        .demo-uni-row {
+            margin-bottom: px2rpx(16);
+
+            :deep(.form-label) {
+                margin: 0 !important;
+            }
+
+            :deep(.form-input) {
+                border: none !important;
+            }
+
+            .avatar-section {
+                display: flex;
+                flex-direction: column;
+                align-items: center;
+                gap: px2rpx(12);
+                padding: px2rpx(16);
+
+                .cwg-upload {
+                    width: px2rpx(120);
+                    height: px2rpx(120);
+                    border-radius: 50%;
+                    margin: 0 auto;
+                }
+
+                .text {
+                    font-size: px2rpx(20);
+                    font-weight: 700;
+                    color: var(--color-navy-900);
+                }
+
+                .btn-primary {
+                    min-width: px2rpx(120);
+                    background-color: var(--color-navy-900);
+                    color: white;
+                    padding: px2rpx(12);
+                    border: none;
+                    font-size: px2rpx(14);
+                    text-align: center;
+                    cursor: pointer;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    gap: px2rpx(8);
+
+                    &:active {
+                        background-color: var(--color-navy-700);
+                    }
+                }
+
+                .cid {
+                    color: var(--color-error);
+                    font-size: px2rpx(12);
+                    margin-top: px2rpx(4);
+                }
+            }
+        }
+    }
+
+    .btns {
+        display: flex;
+        justify-content: flex-end;
+        gap: 30px;
+        margin-top: 30px;
+
+        .btn-primary {
+            min-width: px2rpx(120);
+            background-color: var(--color-navy-900);
+            color: white;
+            padding: 12px;
+            border-radius: 8px;
+            border: none;
+            font-size: 14px;
+            text-align: center;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            gap: 8px;
+
+            &:active {
+                background-color: var(--color-navy-700);
+            }
+        }
+    }
+}
+
+:deep(.uni-row1) {
+    .uni-col {
+        padding: 0 10px !important;
+    }
+
+    .base-info-form>span {
+        display: contents;
+    }
+
+    .uni-forms-item {
+        min-height: px2rpx(79);
+        margin-bottom: px2rpx(10);
+    }
+
+    .uni-select,
+    .uni-combox,
+    .uni-easyinput__content,
+    .uni-date-editor--x {
+        border: none !important;
+        background-color: var(--color-zinc-100) !important;
+    }
+
+    .uni-date-x {
+        border: none !important;
+        background-color: rgba(195, 195, 195, 0) !important;
+    }
+}
+</style>

+ 275 - 0
pages/mine/components/SecurityCenterTab.vue

@@ -0,0 +1,275 @@
+<template>
+    <view class="user-form crm-form">
+        <uni-row class="demo-uni-row uni-row1">
+            <uni-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
+                <view class="content-title">
+                    <view v-t="'PersonalManagement.Title.CustomerZonePasswordChange'"></view>
+                </view>
+                <uni-forms :model="passwordInfo" labelWidth="200" label-position="top">
+                    <uni-row class="demo-uni-row">
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.OldPassword')">
+                                <uni-easyinput :clearable="false" v-model="passwordInfo.oldPassword"
+                                    :placeholder="locale == 'es' ? 'Introduzca el nombre de la red' : t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.NewPassword')">
+                                <uni-easyinput :clearable="false" v-model="passwordInfo.newPassword"
+                                    :placeholder="locale == 'es' ? 'Introduzca nueva contraseña' : t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.NewPasswordConfirmation')">
+                                <uni-easyinput :clearable="false" v-model="passwordInfo.checkPass"
+                                    :placeholder="locale == 'es' ? 'Introduzca nueva contraseña' : t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <view class="notice-list">
+                                <view v-for="(item, index) in noticeItems" :key="index"
+                                    :class="['notice-item', item.valid ? 'isOK' : '']">
+                                    {{ item.label }}
+                                </view>
+                            </view>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <view class="btn btn-confirm" @click="passwordUpdate">{{ locale == 'es' ? 'Actualizar contraseña' : t('Btn.Application') }}</view>
+                        </uni-col>
+                    </uni-row>
+                </uni-forms>
+            </uni-col>
+            <uni-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
+                <view class="content-title">
+                    <view v-t="'PersonalManagement.Title.EmailChange'"></view>
+                </view>
+                <uni-forms :model="emailInfo" labelWidth="200" label-position="top">
+                    <uni-row class="demo-uni-row">
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.OldEmail')">
+                                <uni-easyinput :clearable="false" v-model="emailInfo.oldEmail"
+                                    :placeholder="t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <uni-forms-item :label="t('PersonalManagement.Label.NewEmail')">
+                                <uni-easyinput :clearable="false" v-model="emailInfo.email"
+                                    :placeholder="t('placeholder.input')" />
+                            </uni-forms-item>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <view class="email-code">
+                                <uni-forms-item :label="t('PersonalManagement.Label.MailboxVerificationCode')"
+                                    class="email-code-item">
+                                    <uni-easyinput :clearable="false" v-model="emailInfo.emailCode"
+                                        :placeholder="locale == 'es' ? 'Código de 6 dígitos' : t('placeholder.input')" />
+                                </uni-forms-item>
+                                <view class="btn btn-code" @click="handleGetCode">{{ getCodeString }}</view>
+                            </view>
+                        </uni-col>
+                        <uni-col :xs="24">
+                            <view class="btn btn-confirm" @click="emailUpdate">{{ locale == 'es' ? 'Actualizar contraseña' : t('Btn.Application') }}</view>
+                        </uni-col>
+                    </uni-row>
+                </uni-forms>
+            </uni-col>
+        </uni-row>
+    </view>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, onMounted } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { showToast } from "@/utils/toast";
+import { personalApi } from '@/service/personal';
+import { useEmailCountdown } from '@/hooks/useEmailCountdown';
+import config from '@/config';
+const {
+    time,
+    text: getCodeString,
+    canSend,
+    start,
+    restore
+} = useEmailCountdown()
+const { t, locale } = useI18n();
+interface BankListType {
+    key: string;
+    label: string;
+    icon: string;
+}
+const passwordInfo = ref({
+    oldPassword: '',
+    newPassword: '',
+    checkPass: ''
+});
+const emailInfo = ref({
+    email: '',
+});
+const rule1 = computed(() => {
+    if (!passwordInfo.value.newPassword) {
+        return false;
+    }
+    return /^.{8,16}$/.test(passwordInfo.value.newPassword);
+});
+const rule2 = computed(() => {
+    return /^(?=.*?[a-z])(?=.*?[A-Z]).*$/.test(passwordInfo.value.newPassword);
+});
+const rule3 = computed(() => {
+    return /^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?!.*([~!@&%$^\\(\\)#_]).*\\1.*\\1)[A-Za-z0-9~!@&%$^\\(\\)#_]{8,16}$/.test(
+        passwordInfo.value.newPassword
+    );
+});
+const noticeItems = computed(() => [
+    { label: t('signup.form.rules.1st'), valid: rule1.value },
+    { label: t('signup.form.rules.2nd'), valid: rule2.value },
+    { label: t('signup.form.rules.3rd'), valid: rule3.value }
+])
+// 发送邮箱验证码
+async function sendEmailCode() {
+    try {
+        const res = await personalApi.customUpdateEmailCode({
+            email: emailInfo.value.email,
+            oldEmail: emailInfo.value.oldEmail,
+        });
+        if (res.code === 200) {
+            start()
+            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 (!emailInfo.value.email) {
+        showToast(t("vaildate.email.empty"));
+        return;
+    }
+    if (!config.Pattern.Email.test(emailInfo.value.email)) {
+        showToast(t("vaildate.email.format"));
+        return;
+    }
+
+    if (!canSend.value) return
+    await sendEmailCode();
+}
+// 提交修改邮箱
+async function emailUpdate() {
+    try {
+        const res = await personalApi.customUpdateEmail({
+            ...emailInfo.value
+        });
+        if (res.code === 200) {
+            start()
+            showToast(t("Msg.Success"));
+        } else {
+            showToast(res.msg);
+        }
+    } catch (error: any) {
+        showToast(error.msg);
+    }
+}
+// 提交修改密码
+async function passwordUpdate() {
+    try {
+        if (!rule1.value) {
+            showToast(t("signup.form.rules.1st"));
+            return;
+        }
+        if (!rule2.value) {
+            showToast(t("signup.form.rules.2nd"));
+            return;
+        }
+        if (!rule3.value) {
+            showToast(t("signup.form.rules.3rd"));
+            return;
+        }
+        const res = await personalApi.customUpdatePassword({
+            ...passwordInfo.value
+        });
+        if (res.code === 200) {
+            start()
+            showToast(t("Msg.Success"));
+        } else {
+            showToast(res.msg);
+        }
+    } catch (error: any) {
+        showToast(error.msg);
+    }
+}
+onMounted(() => {
+    restore()
+})
+
+
+</script>
+
+
+
+<style scoped lang="scss">
+:deep(.uni-row1) {
+    .uni-col {
+        padding: 0 10px !important;
+    }
+
+    .uni-forms-item {
+        min-height: px2rpx(79);
+        margin-bottom: px2rpx(10);
+    }
+
+    .uni-easyinput__content {
+        border: none !important;
+        background-color: var(--color-zinc-100) !important;
+    }
+}
+
+.btn {
+    width: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: px2rpx(12) 0;
+    background-color: var(--color-error);
+    color: white;
+    border: none;
+    font-size: px2rpx(16);
+    cursor: pointer;
+    margin-bottom: px2rpx(24);
+}
+
+.notice-list {
+    margin: px2rpx(10) 0;
+    padding: 0 px2rpx(12) px2rpx(12) 0;
+
+    .notice-item {
+        font-size: px2rpx(14);
+        color: var(--color-yellow-800);
+        line-height: px2rpx(24);
+    }
+
+    .isOK {
+        color: var(--color-success);
+    }
+}
+
+.email-code {
+    display: flex;
+    align-items: flex-end;
+
+    .email-code-item {
+        flex: 1;
+    }
+
+    .btn-code {
+        margin-bottom: px2rpx(18);
+        width: auto;
+        padding: px2rpx(10) px2rpx(16);
+    }
+
+}
+</style>

+ 0 - 276
pages/mine/components/VerificationRow.vue

@@ -1,276 +0,0 @@
-<template>
-    <view>
-        <view :class="['verification-row', { 'verification-row-last': isLast && !idFrontUrl }]">
-            <view class="verification-icon">
-                <cwg-icon :name="icon" :size="20" color="#9ca3af" />
-            </view>
-            <view class="verification-content">
-                <view class="verification-label-wrapper">
-                    <text class="verification-label">{{ label }}</text>
-                    <text v-if="idType" class="verification-id-type">({{ idType }})</text>
-                </view>
-                <view class="verification-status-wrapper">
-                    <cwg-icon :name="statusIcon" :size="18" :color="statusColor" />
-                    <text :class="['verification-status-text', statusClass]">
-                        {{ statusText }}
-                    </text>
-                    <text v-if="date" class="verification-date">• {{ date }}</text>
-                </view>
-            </view>
-            <view class="verification-arrow">
-                <cwg-icon name="right" :size="20" color="#d1d5db" />
-            </view>
-        </view>
-
-        <!-- ID Card Images -->
-        <view v-if="idFrontUrl || idBackUrl || idHoldUrl" :class="['id-card-images', { 'id-card-last': isLast }]">
-            <!-- <view class="id-card-label">
-                <text class="id-card-label-text">{{ t('card.Form.f21') }}</text>
-            </view> -->
-            <view class="id-card-container">
-                <view v-if="idFrontUrl" class="id-card-item" @click="previewImage(absoluteUrl(idFrontUrl))">
-                    <image :src="absoluteUrl(idFrontUrl)" class="id-card-image" mode="aspectFill" />
-                    <text class="id-card-text">{{ t('card.Form.f21') }}</text>
-                </view>
-                <view v-if="idBackUrl" class="id-card-item" @click="previewImage(absoluteUrl(idBackUrl))">
-                    <image :src="absoluteUrl(idBackUrl)" class="id-card-image" mode="aspectFill" />
-                    <text class="id-card-text">{{ t('card.Form.f22') }}</text>
-                </view>
-                <view v-if="idHoldUrl" class="id-card-item" @click="previewImage(absoluteUrl(idHoldUrl))">
-                    <image :src="absoluteUrl(idHoldUrl)" class="id-card-image" mode="aspectFill" />
-                    <text class="id-card-text">{{ t('card.Form.f23') }}</text>
-                </view>
-            </view>
-        </view>
-    </view>
-</template>
-
-<script setup lang="ts">
-import { computed } from 'vue';
-import { useI18n } from "vue-i18n";
-import config from "@/config";
-
-const { t } = useI18n();
-
-function absoluteUrl(url: string) {
-    if (!url) {
-        return "";
-    }
-    if (/^https?:\/\//.test(url)) {
-        return url;
-    }
-    const prefix = config.Host85 || "";
-    if (!prefix) {
-        return url;
-    }
-    return `${prefix}${url.startsWith("/") ? url : `/${url}`}`;
-}
-
-// 图片预览功能
-function previewImage(url: string) {
-    if (!url) return;
-
-    // 使用 uni.previewImage 进行图片预览
-    uni.previewImage({
-        urls: [url],
-        longPressActions: {
-            itemList: [t('card.vaildate.v30')], // 保存图片
-            success: function (data) {
-                //  console.log('选中了第' + (data.tapIndex + 1) + '个按钮,第' + (data.index + 1) + '张图片');
-            },
-            fail: function (err) {
-                //  console.log(err.errMsg);
-            }
-        },
-        showMenu: false
-    });
-}
-
-interface Props {
-    icon: string;
-    label: string;
-    status: 'verified' | 'unverified' | 'pending';
-    date?: string;
-    idType?: string;
-    idFrontUrl?: string;
-    idBackUrl?: string;
-    idHoldUrl?: string;
-    isLast?: boolean;
-}
-
-const props = defineProps<Props>();
-
-const statusIcon = computed(() => {
-    switch (props.status) {
-        case 'verified':
-            return 'verified';
-        case 'pending':
-            return 'info';
-        case 'unverified':
-            return 'closeempty';
-        default:
-            return 'closeempty';
-    }
-});
-
-const statusColor = computed(() => {
-    switch (props.status) {
-        case 'verified':
-            return '#22c55e';
-        case 'pending':
-            return '#eab308';
-        case 'unverified':
-            return '#9ca3af';
-        default:
-            return '#9ca3af';
-    }
-});
-
-const statusText = computed(() => {
-    switch (props.status) {
-        case 'verified':
-            return t('card.Status.t1'); // 成功/已认证
-        case 'pending':
-            return t('card.Status.t3'); // 处理中/待审核
-        case 'unverified':
-            return t('card.Status.t4'); // 待认证
-        default:
-            return t('card.Status.t4'); // 待认证
-    }
-});
-
-const statusClass = computed(() => {
-    switch (props.status) {
-        case 'verified':
-            return 'status-verified';
-        case 'pending':
-            return 'status-pending';
-        case 'unverified':
-            return 'status-unverified';
-        default:
-            return 'status-unverified';
-    }
-});
-</script>
-
-<style scoped lang="scss">
-@import "@/uni.scss";
-
-.verification-row {
-    display: flex;
-    align-items: center;
-    gap: px2rpx(12);
-    padding: px2rpx(12) 0;
-    border-bottom: 1px solid #f9fafb;
-}
-
-.verification-row-last {
-    border-bottom: none;
-}
-
-.verification-icon {
-    flex-shrink: 0;
-}
-
-.verification-content {
-    flex: 1;
-    min-width: 0;
-    display: flex;
-    flex-direction: column;
-}
-
-.verification-label-wrapper {
-    display: flex;
-    align-items: center;
-    gap: px2rpx(4);
-    margin-bottom: px2rpx(4);
-}
-
-.verification-label {
-    font-size: px2rpx(12);
-    color: #6b7280;
-}
-
-.verification-id-type {
-    font-size: px2rpx(10);
-    color: #9ca3af;
-}
-
-.verification-status-wrapper {
-    display: flex;
-    align-items: center;
-    gap: px2rpx(4);
-}
-
-.verification-status-text {
-    font-size: px2rpx(12);
-}
-
-.status-verified {
-    color: #22c55e;
-}
-
-.status-pending {
-    color: #eab308;
-}
-
-.status-unverified {
-    color: #9ca3af;
-}
-
-.verification-date {
-    font-size: px2rpx(10);
-    color: #9ca3af;
-}
-
-.verification-arrow {
-    flex-shrink: 0;
-}
-
-.id-card-images {
-    padding: px2rpx(12) 0;
-    border-bottom: 1px solid #f9fafb;
-}
-
-.id-card-last {
-    border-bottom: none;
-}
-
-.id-card-label {
-    margin-bottom: px2rpx(8);
-}
-
-.id-card-label-text {
-    font-size: px2rpx(12);
-    color: #6b7280;
-}
-
-.id-card-container {
-    display: flex;
-    gap: px2rpx(12);
-}
-
-.id-card-item {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    cursor: pointer;
-}
-
-.id-card-item:active {
-    opacity: 0.8;
-}
-
-.id-card-image {
-    width: px2rpx(80);
-    height: px2rpx(60);
-    border-radius: px2rpx(4);
-    overflow: hidden;
-}
-
-.id-card-text {
-    font-size: px2rpx(10);
-    color: #9ca3af;
-    margin-top: px2rpx(4);
-}
-</style>

+ 64 - 0
pages/mine/components/bank.ts

@@ -0,0 +1,64 @@
+// 银行信息类型定义
+export interface BankCard {
+  id: string | number
+  type: BankType
+  bankUname: string
+  defaultBank: boolean
+  authStatus?: AuthStatus
+  approveStatus?: ApproveStatus
+  
+  // 不同类型特有字段
+  // 加密钱包特有
+  addressName?: string
+  address?: string
+  addressProve?: string
+  cardFiles?: CardFile[]
+  
+  // 银联卡特有
+  bankFront?: string
+  bankName?: string
+  bankCardNum?: string
+  bankBranchName?: string
+  
+  // 电汇特有
+  bankAddr?: string
+  swiftCode?: string
+  bankCode?: string
+  agencyNo?: string
+  
+  // 信用卡特有
+  expiryYearMonth?: string
+  cvv?: string
+}
+
+export interface CardFile {
+  path: string
+  name?: string
+  size?: number
+}
+
+export enum BankType {
+  UNIONPAY = 1,      // 中国银联卡
+  WIRE_TRANSFER = 2, // 银行电汇
+  CREDIT_CARD = 3,   // 信用卡
+  CRYPTO = 4         // 加密钱包
+}
+
+export enum AuthStatus {
+  UNAUTHED = 0,      // 未认证
+  AUTHED = 1         // 已认证
+}
+
+export enum ApproveStatus {
+  PENDING = 1,       // 审核中
+  APPROVED = 2,      // 已通过
+  REJECTED = 3       // 已拒绝
+}
+
+// 银行选项
+export interface BankOption {
+  id: string | number
+  name: string
+  enName: string
+  code?: string
+}

+ 348 - 0
pages/mine/info copy 2.vue

@@ -0,0 +1,348 @@
+<template>
+    <cwg-page-wrapper>
+        <uni-tabs v-model="current" :tabs="tabs" @change="changeTab" color="#333" lineHeight="0" field="name">
+            <template v-slot="{ row, index }">
+                <view class="tab-title " :class="{ active: current === index }">
+                    <cwg-icon v-if="row.icon" :name="row.icon" :size="16"
+                        :color="current === index ? '#fff' : '#333'" />
+                    <text>{{ row.name }}</text>
+                </view>
+            </template>
+        </uni-tabs>
+        <view class="info-card">
+            <view class="content-title">Personal Information</view>
+            <view class="card-body">
+
+                <uni-row class="demo-uni-row">
+                    <uni-col :xs="24" :sm="24" :md="24" :lg="6" :xl="6">
+                        <view class="avatar-section">
+                            <cwg-input v-model:value="formData.idBackUrl" type="upload" fkey="idBackUrl"
+                                rulesKey="idBackUrl" :is-upload-d="true" accept="image/png, image/jpeg, image/jpg"
+                                :readonly="isReadonly" :disabled="isReadonly" @change="handleChange">
+                                <view class="cwg-upload">
+                                </view>
+                            </cwg-input>
+                            <view class="text name">{{ userInfo.firstName }}</view>
+                            <view class="text cid"> CID:{{ userInfo.cId }}</view>
+                            <view class="btn-primary" @click="handleEditProfile">上传头像</view>
+                        </view>
+                    </uni-col>
+                    <uni-col :xs="24" :sm="24" :md="24" :lg="18" :xl="18" class="">
+                        <view class="form">
+                            <uni-row>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
+                                    <cwg-input v-model:value="formData.lastName" fkey="lastName" :required="true"
+                                        :label="t('card.Form.f4')" rulesKey="lastName" :readonly="isReadonly"
+                                        :disabled="isReadonly" @change="handleChange" />
+                                    <cwg-input v-model:value="formData.firstName" fkey="firstName" :required="true"
+                                        :label="t('card.Form.f5')" rulesKey="firstName" :readonly="isReadonly"
+                                        :disabled="isReadonly" @change="handleChange" />
+                                    <cwg-input v-model:value="formData.email" fkey="email" :label="t('card.Form.f3')"
+                                        :required="true" rulesKey="email" :readonly="isReadonly" :disabled="isReadonly"
+                                        @change="handleChange" />
+                                    <view class="f" v-if="phoneCodes.length > 0">
+                                        <cwg-input v-model:value="formData.areaCode" class="l" fkey="areaCode"
+                                            :required="true" type="select" :show-search="true" :columns="phoneCodes"
+                                            :label="t('card.Form.f2')" rulesKey="areaCode" :readonly="isReadonly"
+                                            :disabled="isReadonly" @change="handleChange" />
+                                        <cwg-input v-model:value="formData.mobile" class="r" fkey="mobile" label=" "
+                                            rulesKey="mobile" :readonly="isReadonly" :disabled="isReadonly"
+                                            @change="handleChange" />
+                                    </view>
+                                    <cwg-input v-model:value="formData.gender" fkey="gender" type="select"
+                                        :required="true" rulesKey="gender" :columns="sexOptions"
+                                        :label="t('card.Form.f8')" :readonly="isReadonly" :disabled="isReadonly"
+                                        @change="handleChange" />
+                                    <cwg-input v-model:value="formData.birthday" :required="true" type="date"
+                                        fkey="birthday" rulesKey="birthday" :label="t('card.Form.f6')"
+                                        :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
+                                    <view class="form-item ">
+                                        <view class="left-bg">
+                                            <text class="form-label">User CID</text>
+                                            <text class="form-value">{{ userInfo.userId }}</text>
+                                        </view>
+                                    </view>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
+                                    <view class="form-item ">
+                                        <view class="left-bg">
+                                            <text class="form-label">User CID</text>
+                                            <text class="form-value">{{ userInfo.userId }}</text>
+                                        </view>
+                                    </view>
+                                </uni-col>
+                                <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
+                                    <view class="form-item ">
+                                        <view class="left-bg">
+                                            <text class="form-label">User CID</text>
+                                            <text class="form-value">{{ userInfo.userId }}</text>
+                                        </view>
+                                    </view>
+                                </uni-col>
+                            </uni-row>
+                        </view>
+
+                    </uni-col>
+                </uni-row>
+                <uni-row class="demo-uni-row uni-row1">
+                    <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
+                        <cwg-input v-model:value="formData.lastName" fkey="lastName" :required="false"
+                            :label="t('card.Form.f4')" rulesKey="lastName" :readonly="isReadonly" :disabled="isReadonly"
+                            @change="handleChange" />
+                    </uni-col>
+                    <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
+                        <cwg-input v-model:value="formData.firstName" fkey="firstName" :required="false"
+                            :label="t('card.Form.f5')" rulesKey="firstName" :readonly="isReadonly"
+                            :disabled="isReadonly" @change="handleChange" />
+                    </uni-col>
+                    <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
+                        <cwg-input v-model:value="formData.email" fkey="email" :label="t('card.Form.f3')"
+                            :required="false" rulesKey="email" :readonly="isReadonly" :disabled="isReadonly"
+                            @change="handleChange" />
+                    </uni-col>
+                    <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
+                        <cwg-input v-model:value="formData.mobile" class="r" fkey="mobile" :label="t('card.Form.f2')"
+                            rulesKey="mobile" :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
+                    </uni-col>
+
+                    <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
+                        <cwg-input v-model:value="formData.birthday" :required="false" type="date" fkey="birthday"
+                            rulesKey="birthday" :label="t('card.Form.f6')" :readonly="isReadonly" :disabled="isReadonly"
+                            @change="handleChange" />
+                    </uni-col>
+                    <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
+                        <cwg-input v-if="countryOptions.length > 0" v-model:value="formData.nationality"
+                            fkey="nationality" type="select" :required="false" :show-search="true"
+                            rulesKey="nationality" :columns="countryOptions" :label="t('card.Form.f7')"
+                            :readonly="isReadonly" :disabled="isReadonly" @change="handleChange" />
+
+                    </uni-col>
+                    <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
+                        <cwg-input v-if="cityOptions.length > 0" v-model:value="formData.town" fkey="town"
+                            :required="false" :show-search="true" rulesKey="town" type="select" :columns="cityOptions"
+                            :label="t('card.Form.f9')" :readonly="isReadonly" :disabled="isReadonly"
+                            @change="handleChange" />
+
+                    </uni-col>
+                    <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
+                        <cwg-input v-model:value="formData.address" fkey="address" :label="t('card.Form.f10')"
+                            :required="false" rulesKey="address" :readonly="isReadonly" :disabled="isReadonly"
+                            @change="handleChange" />
+
+                    </uni-col>
+                    <uni-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
+                        <cwg-input v-model:value="formData.postCode" :required="false" fkey="postCode"
+                            rulesKey="postCode" :label="t('card.Form.f11')" :readonly="isReadonly"
+                            :disabled="isReadonly" @change="handleChange" />
+                    </uni-col>
+                </uni-row>
+            </view>
+        </view>
+    </cwg-page-wrapper>
+</template>
+
+<script setup lang="ts">
+import { onLoad } from '@dcloudio/uni-app'
+import { ref, onMounted, watch, computed, reactive } from "vue";
+import InfoRow from './components/InfoRow.vue';
+import VerificationRow from './components/VerificationRow.vue';
+import { useI18n } from "vue-i18n";
+import useUserStore from "@/stores/use-user-store";
+import useRouter from "@/hooks/useRouter";
+import useIdTypeOptions from "@/composables/useIdTypeOptions";
+const { allIdTypeOptions } = useIdTypeOptions()
+const userStore = useUserStore();
+const userInfo = computed(() => userStore.userInfo);
+const { t } = useI18n();
+const router = useRouter();
+
+const idTypeOptions = computed(() => {
+    return allIdTypeOptions.value?.filter(item => item.value === userInfo.value?.idType);
+});
+// 获取身份认证状态
+function getIdentityStatus() {
+    // 认证成功就是已认证;否则视为审核中(pending)
+    if (userInfo.value?.approveStatus == 2 || userInfo.value?.kycStatus == 2) {
+        return 'verified';
+    }
+    return 'pending';
+}
+const formData = ref({})
+const imageStyles = ref({
+    width: '80px',
+    height: '80px',
+    borderRadius: '50%'
+});
+const current = ref(0);
+const tabs = computed(() => [
+    { id: 1, name: t('card.Info.s1'), icon: 'icon_personal certification' },
+    { id: 2, name: t('ImproveImmediately.Title.AddressInformation'), icon: 'dw' },
+    { id: 3, name: t('card.Info.s0'), icon: 'globe' },
+    { id: 4, name: t('card.Info.s2'), icon: 'checkmark' }
+]);
+const changeTab = (index) => {
+    current.value = index;
+}
+
+// 获取身份认证日期
+function getIdentityDate() {
+    // 如果有证件有效期,则使用证件签发日期
+    if (userInfo.value?.issueDate) {
+        return new Date(userInfo.value.issueDate).toLocaleDateString();
+    }
+    return '';
+}
+
+const handleEditProfile = () => {
+    router.push('/pages/mine/improve');
+};
+
+const handleChangePassword = () => {
+    uni.showToast({
+        title: t('card.Msg.ComingSoon'),
+        icon: 'none'
+    });
+};
+const isReadonly = computed(() => isAuthInfo.value);
+const listStyles = ref({
+    borderStyle: {
+        color: '#eee',
+        width: '1px',
+        style: 'solid',
+        radius: '5px'
+    },
+    "border": false, // 是否显示边框
+    "dividline": true // 是否显示分隔线
+});
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.tab-title {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(4);
+    border: 1px solid #f3f4f6;
+    font-size: 18px;
+    padding: px2rpx(8) px2rpx(16);
+    border-radius: px2rpx(8);
+    background-color: white;
+}
+
+.active {
+    background-color: var(--main-yellow);
+    color: #fff;
+    border: none;
+}
+
+.info-card {
+    border-radius: px2rpx(8);
+    background-color: white;
+    // padding: px2rpx(16);
+}
+
+.card-body {
+    padding: px2rpx(16);
+}
+
+.demo-uni-row {
+    margin-bottom: px2rpx(16);
+}
+
+.avatar-section {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: px2rpx(12);
+    padding: px2rpx(16);
+
+    .cwg-upload {
+        width: px2rpx(120);
+        height: px2rpx(120);
+        border-radius: 50%;
+        margin: 0 auto;
+    }
+
+    .text {
+        font-size: 14px;
+        color: #333;
+    }
+
+    .btn-primary {
+        margin-top: px2rpx(12);
+        min-width: px2rpx(120);
+        background-color: #2563eb;
+        color: white;
+        padding: px2rpx(12);
+        border-radius: px2rpx(8);
+        border: none;
+        font-size: px2rpx(14);
+        text-align: center;
+    }
+
+    .btn-primary:active {
+        background-color: #1d4ed8;
+    }
+
+    .cid {
+        color: var(--main-yellow);
+        font-size: 12px;
+        margin-top: px2rpx(4);
+    }
+}
+
+
+
+
+
+
+.uni-row1 {
+    display: flex;
+    gap: px2rpx(16);
+    flex-shrink: 1;
+
+    .uni-col {
+
+        margin: px2rpx(12);
+        border-radius: px2rpx(4);
+        display: flex;
+        flex-direction: column;
+        gap: px2rpx(16);
+    }
+}
+
+.left-bg {
+    padding: px2rpx(8);
+    background-color: #f9fafb;
+}
+
+.form {
+    padding: px2rpx(12);
+    border-radius: px2rpx(4);
+    display: flex;
+    flex-direction: column;
+    gap: px2rpx(16);
+
+}
+
+.form-item {
+    // 
+    border-radius: px2rpx(8);
+    padding: px2rpx(8);
+}
+
+.form-label {
+    font-size: px2rpx(12);
+    color: #6b7280;
+    font-weight: 500;
+}
+
+.form-value {
+    font-size: px2rpx(14);
+    color: #1f2937;
+    font-weight: 500;
+}
+</style>

+ 238 - 0
pages/mine/info copy.vue

@@ -0,0 +1,238 @@
+<template>
+    <cwg-page-wrapper>
+        <view class="user-profile-detail">
+            <!-- Content -->
+            <view class="content">
+                <!-- Basic Information -->
+                <view class="info-card">
+                    <view class="card-header">
+                        <cwg-icon name="icon_personal certification" :size="20" color="#2563eb" />
+                        <text class="card-title">{{ t('card.Info.s1') }}</text>
+                    </view>
+                    <view class="card-body">
+                        <info-row icon="xm" :label="t('card.Form.f5') + ' ' + t('card.Form.f4')"
+                            :value="userInfo.firstName + ' ' + userInfo.lastName" />
+                        <info-row icon="email-outline" :label="t('card.Form.f3')" :value="userInfo.email" />
+                        <info-row icon="phone" :label="t('card.Form.f2')"
+                            :value="userInfo.areaCode + ' ' + userInfo.mobile" />
+                        <info-row icon="cwg-calendar" :label="t('card.Form.f6')" :value="userInfo.birthday" />
+                        <info-row :icon="userInfo.gender == 'M' ? 'nan' : 'nv'" :label="t('card.Form.f8')"
+                            :value="userInfo.gender == 'M' ? t('card.Form.v1') : t('card.Form.v2')" :isLast="true" />
+                    </view>
+                </view>
+
+                <!-- Location Information -->
+                <view class="info-card">
+                    <view class="card-header">
+                        <cwg-icon name="dw" :size="20" color="#16a34a" />
+                        <text class="card-title">{{ t('ImproveImmediately.Title.AddressInformation') }}</text>
+                    </view>
+                    <view class="card-body">
+                        <info-row icon="gj" :label="t('card.Form.f7')" :value="userInfo.countryCnName" />
+                        <info-row icon="dw" :label="t('card.Form.f9')" :value="userInfo.townEnName" />
+                        <info-row icon="xxdz" :label="t('card.Form.f10')" :value="userInfo.address" />
+                        <info-row icon="paperclip" :label="t('card.Form.f11')" :value="userInfo.postCode"
+                            :isLast="true" />
+                    </view>
+                </view>
+
+                <!-- Career Information -->
+                <view class="info-card">
+                    <view class="card-header">
+                        <cwg-icon name="globe" :size="20" color="#9333ea" />
+                        <text class="card-title">{{ t('card.Info.s0') }}</text>
+                    </view>
+                    <view class="card-body">
+                        <info-row icon="globe" :label="t('card.Form.f12')" :value="userInfo.occupationDesc" />
+                        <info-row icon="location" :label="t('card.Form.f13')" :value="userInfo.annualSalary" />
+                        <info-row icon="globe" :label="t('card.Form.f14')" :value="userInfo.accountPurpose" />
+                        <info-row icon="gzcalendar" :label="t('card.Form.f15')" :value="userInfo.expectedMonthlyVolume"
+                            :isLast="true" />
+                    </view>
+                </view>
+
+                <!-- Identity Verification -->
+                <view class="info-card">
+                    <view class="card-header">
+                        <cwg-icon name="checkmark" :size="20" color="#ea580c" />
+                        <text class="card-title">{{ t('card.Info.s2') }}</text>
+                    </view>
+                    <view class="card-body">
+                        <verification-row icon="checkmarkempty" :label="t('card.Form.f16')"
+                            :status="getIdentityStatus()" :date="getIdentityDate()" :idType="idTypeOptions[0]?.text"
+                            :idFrontUrl="userInfo.idFrontUrl" :idBackUrl="userInfo.idBackUrl"
+                            :idHoldUrl="userInfo.idHoldUrl" :isLast="true" />
+                    </view>
+                </view>
+
+                <!-- Action Buttons -->
+                <!-- <view class="action-buttons">
+                    <button class="btn-primary" @click="handleEditProfile">{{ t('card.Btn.Edit') }}</button>
+                    <button class="btn-secondary" @click="handleChangePassword">{{ t('card.Btn.ChangePwd') }}</button>
+                </view> -->
+            </view>
+        </view>
+    </cwg-page-wrapper>
+</template>
+
+<script setup lang="ts">
+import { onLoad } from '@dcloudio/uni-app'
+import { ref, onMounted, watch, computed, reactive } from "vue";
+import InfoRow from './components/InfoRow.vue';
+import VerificationRow from './components/VerificationRow.vue';
+import { useI18n } from "vue-i18n";
+import useUserStore from "@/stores/use-user-store";
+import useRouter from "@/hooks/useRouter";
+import useIdTypeOptions from "@/composables/useIdTypeOptions";
+const { allIdTypeOptions } = useIdTypeOptions()
+const userStore = useUserStore();
+const userInfo = computed(() => userStore.userInfo);
+const { t } = useI18n();
+const router = useRouter();
+
+const idTypeOptions = computed(() => {
+    return allIdTypeOptions.value?.filter(item => item.value === userInfo.value?.idType);
+});
+// 获取身份认证状态
+function getIdentityStatus() {
+    // 认证成功就是已认证;否则视为审核中(pending)
+    if (userInfo.value?.approveStatus == 2 || userInfo.value?.kycStatus == 2) {
+        return 'verified';
+    }
+    return 'pending';
+}
+
+// 获取身份认证日期
+function getIdentityDate() {
+    // 如果有证件有效期,则使用证件签发日期
+    if (userInfo.value?.issueDate) {
+        return new Date(userInfo.value.issueDate).toLocaleDateString();
+    }
+    return '';
+}
+
+const handleEditProfile = () => {
+    router.push('/pages/mine/improve');
+};
+
+const handleChangePassword = () => {
+    uni.showToast({
+        title: t('card.Msg.ComingSoon'),
+        icon: 'none'
+    });
+};
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.page-wrapper {
+    padding: 0;
+}
+
+.user-profile-detail {
+    min-height: 100vh;
+    background-color: #f9fafb;
+}
+
+.header {
+    background: linear-gradient(to right, #2563eb, #60a5fa);
+    padding: px2rpx(16);
+}
+
+.header-content {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(8);
+}
+
+.avatar {
+    width: px2rpx(40);
+    height: px2rpx(40);
+    border-radius: 50%;
+    background-color: white;
+    border: px2rpx(2) solid white;
+    box-shadow: 0 px2rpx(2) px2rpx(6) rgba(0, 0, 0, 0.1);
+}
+
+.header-info {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+}
+
+.user-name {
+    color: white;
+    font-size: px2rpx(18);
+    margin-bottom: px2rpx(2);
+}
+
+.user-email {
+    color: #bfdbfe;
+    font-size: px2rpx(12);
+}
+
+.content {
+    padding: px2rpx(1) px2rpx(16) px2rpx(24);
+}
+
+.info-card {
+    background-color: white;
+    border-radius: px2rpx(8);
+    box-shadow: 0 px2rpx(1) px2rpx(4) rgba(0, 0, 0, 0.1);
+    margin-top: px2rpx(16);
+    overflow: hidden;
+}
+
+.card-header {
+    display: flex;
+    align-items: center;
+    gap: px2rpx(4);
+    padding: px2rpx(12) px2rpx(16);
+    border-bottom: 1px solid #f3f4f6;
+}
+
+.card-title {
+    color: #1f2937;
+    font-size: px2rpx(14);
+}
+
+.card-body {
+    padding: px2rpx(16);
+}
+
+.action-buttons {
+    margin-top: px2rpx(24);
+    display: flex;
+    flex-direction: column;
+    gap: px2rpx(12);
+}
+
+.btn-primary {
+    width: 100%;
+    background-color: #2563eb;
+    color: white;
+    padding: px2rpx(12);
+    border-radius: px2rpx(8);
+    border: none;
+    font-size: px2rpx(14);
+}
+
+.btn-primary:active {
+    background-color: #1d4ed8;
+}
+
+.btn-secondary {
+    width: 100%;
+    background-color: white;
+    color: #374151;
+    padding: px2rpx(12);
+    border-radius: px2rpx(8);
+    border: 1px solid #d1d5db;
+    font-size: px2rpx(14);
+}
+
+.btn-secondary:active {
+    background-color: #f9fafb;
+}
+</style>

+ 342 - 180
pages/mine/info.vue

@@ -1,238 +1,400 @@
 <template>
     <cwg-page-wrapper>
-        <view class="user-profile-detail">
-            <!-- Content -->
-            <view class="content">
-                <!-- Basic Information -->
-                <view class="info-card">
-                    <view class="card-header">
-                        <cwg-icon name="icon_personal certification" :size="20" color="#2563eb" />
-                        <text class="card-title">{{ t('card.Info.s1') }}</text>
-                    </view>
-                    <view class="card-body">
-                        <info-row icon="xm" :label="t('card.Form.f5') + ' ' + t('card.Form.f4')"
-                            :value="userInfo.firstName + ' ' + userInfo.lastName" />
-                        <info-row icon="email-outline" :label="t('card.Form.f3')" :value="userInfo.email" />
-                        <info-row icon="phone" :label="t('card.Form.f2')"
-                            :value="userInfo.areaCode + ' ' + userInfo.mobile" />
-                        <info-row icon="cwg-calendar" :label="t('card.Form.f6')" :value="userInfo.birthday" />
-                        <info-row :icon="userInfo.gender == 'M' ? 'nan' : 'nv'" :label="t('card.Form.f8')"
-                            :value="userInfo.gender == 'M' ? t('card.Form.v1') : t('card.Form.v2')" :isLast="true" />
-                    </view>
-                </view>
-
-                <!-- Location Information -->
-                <view class="info-card">
-                    <view class="card-header">
-                        <cwg-icon name="dw" :size="20" color="#16a34a" />
-                        <text class="card-title">{{ t('ImproveImmediately.Title.AddressInformation') }}</text>
-                    </view>
-                    <view class="card-body">
-                        <info-row icon="gj" :label="t('card.Form.f7')" :value="userInfo.countryCnName" />
-                        <info-row icon="dw" :label="t('card.Form.f9')" :value="userInfo.townEnName" />
-                        <info-row icon="xxdz" :label="t('card.Form.f10')" :value="userInfo.address" />
-                        <info-row icon="paperclip" :label="t('card.Form.f11')" :value="userInfo.postCode"
-                            :isLast="true" />
-                    </view>
+        <uni-tabs v-model="current" :tabs="tabs" @change="changeTab" color="#333" lineHeight="0" field="name">
+            <template v-slot="{ row, index }">
+                <view class="tab-title " :class="{ active: current == row.id }">
+                    <cwg-icon v-if="row.icon" :name="row.icon" :size="16"
+                        :color="current == row.id ? '#fff' : '#333'" />
+                    <text>{{ row.name }}</text>
                 </view>
-
-                <!-- Career Information -->
-                <view class="info-card">
-                    <view class="card-header">
-                        <cwg-icon name="globe" :size="20" color="#9333ea" />
-                        <text class="card-title">{{ t('card.Info.s0') }}</text>
-                    </view>
-                    <view class="card-body">
-                        <info-row icon="globe" :label="t('card.Form.f12')" :value="userInfo.occupationDesc" />
-                        <info-row icon="location" :label="t('card.Form.f13')" :value="userInfo.annualSalary" />
-                        <info-row icon="globe" :label="t('card.Form.f14')" :value="userInfo.accountPurpose" />
-                        <info-row icon="gzcalendar" :label="t('card.Form.f15')" :value="userInfo.expectedMonthlyVolume"
-                            :isLast="true" />
+            </template>
+        </uni-tabs>
+        <view class="info-card">
+            <view class="content-title" v-if="current != 3 && current != 4">
+                <view>{{ tabs[current - 1].name }}</view>
+                <view class="content-title-btns">
+                    <view v-if="current == 3 && !isSHowIdentity" class="btn-primary" @click="infoSubmit()">
+                        <cwg-icon icon="crm-plus" :size="16" color="#fff" />
+                        <text v-t="'PersonalManagement.Title.ProofOfIdentity'"></text>
                     </view>
-                </view>
-
-                <!-- Identity Verification -->
-                <view class="info-card">
-                    <view class="card-header">
-                        <cwg-icon name="checkmark" :size="20" color="#ea580c" />
-                        <text class="card-title">{{ t('card.Info.s2') }}</text>
+                    <view v-if="current == 3 && !isSHowAddress" class="btn-primary" @click="cancle()">
+                        <cwg-icon icon="crm-plus" :size="16" color="#fff" />
+                        <text v-t="'PersonalManagement.Title.ProofOfAddress'"></text>
                     </view>
-                    <view class="card-body">
-                        <verification-row icon="checkmarkempty" :label="t('card.Form.f16')"
-                            :status="getIdentityStatus()" :date="getIdentityDate()" :idType="idTypeOptions[0]?.text"
-                            :idFrontUrl="userInfo.idFrontUrl" :idBackUrl="userInfo.idBackUrl"
-                            :idHoldUrl="userInfo.idHoldUrl" :isLast="true" />
+                    <view v-if="current == 3" class="btn-primary" @click="cancle()">
+                        <cwg-icon icon="crm-plus" :size="16" color="#fff" />
+                        <text v-t="'PersonalManagement.Title.AttachedFile'"></text>
                     </view>
                 </view>
-
-                <!-- Action Buttons -->
-                <!-- <view class="action-buttons">
-                    <button class="btn-primary" @click="handleEditProfile">{{ t('card.Btn.Edit') }}</button>
-                    <button class="btn-secondary" @click="handleChangePassword">{{ t('card.Btn.ChangePwd') }}</button>
-                </view> -->
+            </view>
+            <view v-if="current == 1">
+                <personal-info-tab v-model="PersonalInformation" :is-readonly="isReadonly" @cancel="cancle"
+                    @next="next(2)" />
+            </view>
+            <view v-if="current == 2">
+                <bank-info-tab />
+            </view>
+            <view v-if="current == 3">
+                <file-management-tab />
+            </view>
+            <view v-if="current == 4">
+                <security-center-tab />
             </view>
         </view>
+        <!-- <view class="form-tab"></view> -->
     </cwg-page-wrapper>
+    <card-websdk-link ref="cardWebsdkLinkRef" />
 </template>
 
 <script setup lang="ts">
-import { onLoad } from '@dcloudio/uni-app'
-import { ref, onMounted, watch, computed, reactive } from "vue";
-import InfoRow from './components/InfoRow.vue';
-import VerificationRow from './components/VerificationRow.vue';
+import { ref, onMounted, watch, computed } from "vue";
+import { pinyin } from "pinyin-pro";
 import { useI18n } from "vue-i18n";
+import { onLoad } from '@dcloudio/uni-app'
+import { userToken } from "@/composables/config";
+import { ucardApi } from "@/api/ucard";
+import { personalApi } from "@/service/personal";
+import { userApi } from "@/api/user";
 import useUserStore from "@/stores/use-user-store";
-import useRouter from "@/hooks/useRouter";
 import useIdTypeOptions from "@/composables/useIdTypeOptions";
-const { allIdTypeOptions } = useIdTypeOptions()
+import CardWebsdkLink from "@/components/card-websdkLink.vue";
+import PersonalInfoTab from "./components/PersonalInfoTab.vue";
+import FileManagementTab from "./components/FileManagementTab.vue";
+import BankInfoTab from "./components/BankInfoTab.vue";
+import SecurityCenterTab from "./components/SecurityCenterTab.vue";
+import useRouter from "@/hooks/useRouter";
+
+const notCountry = [
+    "AF",
+    "AI",
+    "AG",
+    "BS",
+    "BY",
+    "BZ",
+    "BA",
+    "BI",
+    "CF",
+    "CD",
+    "CU",
+    "ET",
+    "FJ",
+    "PS",
+    "GN",
+    "GW",
+    "HT",
+    "IR",
+    "IQ",
+    "LB",
+    "LY",
+    "ML",
+    "MM",
+    "NI",
+    "KP",
+    "PW",
+    "RU",
+    "SO",
+    "SS",
+    "SD",
+    "SY",
+    "UA",
+    "US",
+    "VE",
+    "YE",
+    "ZW",
+]
+const router = useRouter();
 const userStore = useUserStore();
 const userInfo = computed(() => userStore.userInfo);
-const { t } = useI18n();
-const router = useRouter();
-
-const idTypeOptions = computed(() => {
-    return allIdTypeOptions.value?.filter(item => item.value === userInfo.value?.idType);
+const current = ref(0);
+onLoad((options) => {
+    current.value = options?.type;
 });
-// 获取身份认证状态
-function getIdentityStatus() {
-    // 认证成功就是已认证;否则视为审核中(pending)
-    if (userInfo.value?.approveStatus == 2 || userInfo.value?.kycStatus == 2) {
-        return 'verified';
-    }
-    return 'pending';
+const tabs = computed(() => [
+    { id: 1, name: t('PersonalManagement.Title.PersonalInformation'), icon: 'crm-circle-user' },
+    { id: 2, name: t('PersonalManagement.Title.BankInformation'), icon: 'crm-building-columns' },
+    { id: 3, name: t('PersonalManagement.Title.FileManagement'), icon: 'crm-file' },
+    { id: 4, name: t('PersonalManagement.Title.SecurityCenter'), icon: 'crm-lock' }
+]);
+const changeTab = (index) => {
+    router.push(`/pages/mine/info?type=${index + 1}`);
 }
+const { t, locale } = useI18n();
+const cardWebsdkLinkRef = ref<InstanceType<typeof CardWebsdkLink> | null>(null);
+const PersonalInformation = ref({
+    merchantOrderNo: undefined,
+    cardTypeId: undefined,
+    areaCode: undefined,
+    mobile: undefined,
+    email: undefined,
+    firstName: undefined,
+    lastName: undefined,
+    birthday: undefined,
+    nationality: undefined,
+    country: undefined,
+    town: undefined,
+    address: undefined,
+    postCode: undefined,
+    gender: undefined,
+    occupation: undefined,
+    annualSalary: undefined,
+    accountPurpose: undefined,
+    expectedMonthlyVolume: undefined,
+    idType: undefined,
+    idNumber: undefined,
+    ssn: undefined,
+    issueDate: undefined,
+    idNoExpiryDate: undefined,
+    idFrontUrl: undefined,
+    idBackUrl: undefined,
+    idHoldUrl: undefined,
+    ipAddress: undefined,
+    cId: undefined,
+    customId: undefined,
+    photoStatus: '1',
+    kycStatus: '1'
+});
 
-// 获取身份认证日期
-function getIdentityDate() {
-    // 如果有证件有效期,则使用证件签发日期
-    if (userInfo.value?.issueDate) {
-        return new Date(userInfo.value.issueDate).toLocaleDateString();
-    }
-    return '';
-}
+const isUpdate = ref(false);
+const isAuthInfo = ref(false);
 
-const handleEditProfile = () => {
-    router.push('/pages/mine/improve');
-};
+const dialogCheck = ref(false);
+const dialogCheck1 = ref(false);
+const isApprove = computed(() => [2, 4].includes(userInfo.value.approveStatus))
+const countryList = ref<any[]>([]);
 
-const handleChangePassword = () => {
-    uni.showToast({
-        title: t('card.Msg.ComingSoon'),
-        icon: 'none'
-    });
+const getCountry = async () => {
+    const res = await personalApi.Country({});
+    if (res.code === 200) {
+        countryList.value = res.data || [];
+    }
 };
-</script>
 
-<style scoped lang="scss">
-@import "@/uni.scss";
+const cancle = async () => {
+    if (!isApprove.value) {
+        dialogCheck.value = true;
+        dialogCheck1.value = true;
+    } else {
+        router.push({ path: "/customer/index" });
 
-.page-wrapper {
-    padding: 0;
+    }
 }
-
-.user-profile-detail {
-    min-height: 100vh;
-    background-color: #f9fafb;
+const currentUploadCard = computed(() => {
+    if (!PersonalInformation.value.nationality || !countryList.value.length) {
+        return 0;
+    }
+    const selectedCountry = countryList.value.find(
+        (item) => item.code === PersonalInformation.value.nationality
+    );
+    return selectedCountry ? (selectedCountry.uploadCard || 0) : 0;
+})
+const next = async (index) => {
+    // 从 step 2 跳转到 step 3 时,应该直接跳转,不做验证
+    // 只有在 step 3 点击下一步时,才检查是否需要跳过到 step 4
+    if (current.value == 3 && index == 4 && PersonalInformation.value.uploadImage == 0) {
+        // 如果不需要上传图片,跳过 step 3 直接到 step 4
+        index = 4;
+    }
+    // 从 step 3 跳转到 step 4 时,需要验证身份证明相关字段
+    if (index == 4) {
+        // 根据当前选中国籍的 uploadCard 值判断是否需要验证证件照
+        if (currentUploadCard.value === 1) {
+            if (!PersonalInformation.value.cardType) {
+                uni.showToast({ title: t('vaildate.CardType.empty'), icon: 'none' });
+                return;
+            }
+            if (!PersonalInformation.value.fileListID1.path || !PersonalInformation.value.fileListID2.path) {
+                uni.showToast({ title: t('vaildate.IDPhoto.empty'), icon: 'none' });
+                return;
+            }
+        }
+    }
+    router.push({ path: `/pages/mine/info?type=${index}` });
 }
 
-.header {
-    background: linear-gradient(to right, #2563eb, #60a5fa);
-    padding: px2rpx(16);
-}
 
-.header-content {
-    display: flex;
-    align-items: center;
-    gap: px2rpx(8);
-}
 
-.avatar {
-    width: px2rpx(40);
-    height: px2rpx(40);
-    border-radius: 50%;
-    background-color: white;
-    border: px2rpx(2) solid white;
-    box-shadow: 0 px2rpx(2) px2rpx(6) rgba(0, 0, 0, 0.1);
+function handleChange(value: any) {
+    PersonalInformation.value = { ...PersonalInformation.value, [value.key]: value.value };
 }
 
-.header-info {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-}
+const containsChinese = (str: string) => /[\u4E00-\u9FA5]/.test(str);
 
-.user-name {
-    color: white;
-    font-size: px2rpx(18);
-    margin-bottom: px2rpx(2);
-}
+const convertToPinyin = (value: string) =>
+    containsChinese(value)
+        ? pinyin(value, { toneType: "none", type: "capitalize" })
+        : value;
 
-.user-email {
-    color: #bfdbfe;
-    font-size: px2rpx(12);
-}
 
-.content {
-    padding: px2rpx(1) px2rpx(16) px2rpx(24);
+async function getUserSingle() {
+    if (!userToken.value) {
+        uni.showToast({ title: t("common.loginFirst"), icon: 'none' });
+        return;
+    }
+    if (current.value != 1) {
+        return;
+    }
+
+    try {
+        const res = await personalApi.CustomLoginInfo();
+        if (res.code === 200 && res.data) {
+            // 更新表单数据
+            PersonalInformation.value = {
+                ...res.data.customInfo,
+                customType: res.data.customInfo?.customType || 1,
+                addressLines1: res.data.customInfo?.addressLines[0] || '',
+                addressLines2: res.data.customInfo?.addressLines[1] || '',
+            };
+            userStore.saveUserInfo(res.data);
+
+            // 设置状态
+            if (res.data) {
+                isUpdate.value = true;
+            }
+
+            if (res.data.approveStatus == 2 || res.data.kycStatus == 2) {
+                isAuthInfo.value = true;
+            }
+
+            if (res.data.approveStatus == 3) {
+                isAuthInfo.value = false;
+            }
+        }
+        getCountry()
+    } catch (error: any) {
+        console.error('Error in getUserSingle:', error);
+        uni.showToast({ title: error.message || t("common.error"), icon: 'none' });
+    }
 }
 
-.info-card {
-    background-color: white;
-    border-radius: px2rpx(8);
-    box-shadow: 0 px2rpx(1) px2rpx(4) rgba(0, 0, 0, 0.1);
-    margin-top: px2rpx(16);
-    overflow: hidden;
+
+async function infoSubmit() {
+    try {
+        const res = await ucardApi.merchantRegister(PersonalInformation.value);
+        if (res.code === 200) {
+            uni.showToast({ title: t("common.success"), icon: 'success' });
+            // 提交成功后打开 WebSDK 弹窗
+            router.push({
+                path: "/pages/mine/kyc",
+                query: { cardId: (PersonalInformation.value as any).id },
+            });
+        } else {
+            uni.showToast({ title: res.msg, icon: 'none' });
+        }
+    } catch (error: any) {
+        uni.showToast({ title: error.message || t("common.error"), icon: 'none' });
+    }
 }
 
-.card-header {
+const isReadonly = computed(() => isAuthInfo.value);
+
+onMounted(async () => {
+    // 先加载选项数据
+    await Promise.all([
+        getUserSingle(),
+    ]);
+
+});
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.tab-title {
     display: flex;
     align-items: center;
     gap: px2rpx(4);
-    padding: px2rpx(12) px2rpx(16);
-    border-bottom: 1px solid #f3f4f6;
+    border: 1px solid #f3f4f6;
+    font-size: px2rpx(18);
+    padding: px2rpx(8) px2rpx(16);
+    border-radius: px2rpx(2);
+    background-color: white;
 }
 
-.card-title {
-    color: #1f2937;
-    font-size: px2rpx(14);
+.active {
+    background-color: var(--color-error);
+    color: #fff;
+    border: none;
 }
 
-.card-body {
-    padding: px2rpx(16);
-}
+.info-card {
+    .btns {
+        display: flex;
+        justify-content: flex-end;
+        gap: px2rpx(30);
+        margin-top: px2rpx(30);
 
-.action-buttons {
-    margin-top: px2rpx(24);
-    display: flex;
-    flex-direction: column;
-    gap: px2rpx(12);
+        .btn-primary {
+            min-width: px2rpx(120);
+            background-color: var(--color-navy-900);
+            color: white;
+            padding: 0 px2rpx(12);
+            border-radius: px2rpx(8);
+            border: none;
+            font-size: px2rpx(14);
+            text-align: center;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            gap: px2rpx(8);
+        }
+
+        .btn-primary:active {
+            background-color: var(--color-navy-700);
+        }
+    }
 }
 
-.btn-primary {
-    width: 100%;
-    background-color: #2563eb;
-    color: white;
-    padding: px2rpx(12);
-    border-radius: px2rpx(8);
-    border: none;
-    font-size: px2rpx(14);
+.form-tab {
+    height: px2rpx(100);
 }
 
-.btn-primary:active {
-    background-color: #1d4ed8;
+.form-section {
+    margin: px2rpx(8) 0;
 }
 
-.btn-secondary {
+
+
+
+
+
+:deep(.u-uploader) {
     width: 100%;
-    background-color: white;
-    color: #374151;
-    padding: px2rpx(12);
-    border-radius: px2rpx(8);
-    border: 1px solid #d1d5db;
-    font-size: px2rpx(14);
-}
 
-.btn-secondary:active {
-    background-color: #f9fafb;
+    .u-uploader__wrapper {
+        width: 100%;
+        display: block;
+    }
+
+    .u-uploader__preview {
+        width: 100% !important;
+        height: px2rpx(160) !important;
+        border-radius: px2rpx(24);
+        overflow: hidden;
+
+        .u-uploader__preview-image {
+            width: 100%;
+            height: 100%;
+
+            .u-image__img {
+                object-fit: contain;
+            }
+        }
+
+        .u-uploader__preview-delete {
+            position: absolute;
+            top: 0;
+            right: 0;
+            width: px2rpx(30);
+            height: px2rpx(30);
+            border-radius: 0 px2rpx(24) 0 0;
+
+            i {
+                text-align: center;
+                line-height: px2rpx(30);
+                font-size: px2rpx(30);
+            }
+        }
+    }
 }
 </style>

+ 444 - 0
pages/mine/new.vue

@@ -0,0 +1,444 @@
+<template>
+    <cwg-page-wrapper class="create-page" :isHeaderFixed="true">
+        <view class="info-card">
+            <!-- 标题栏 -->
+            <view class="content-title">
+                <view>{{ titleMap[title] }}</view>
+                <view v-if="title == 7" class="btn crm-cursor" @click="backIndex">
+                    <uni-icons type="arrowleft" size="20" color="#666"></uni-icons>
+                    <text>{{ t('Custom.Settings.Title') }}</text>
+                </view>
+            </view>
+            <!-- 内容区域 - 图文类型 -->
+            <view v-if="[1, 2, 3, 5].includes(title)" class="content crm-border-radius">
+                <view v-if="imgContentIf" class="img">
+                    <image :src="imgContent" mode="widthFix" class="content-image" @click="previewImage" />
+                </view>
+                <rich-text :nodes="Content" class="rich-text"></rich-text>
+            </view>
+            <!-- 内容区域 - 视频类型4 -->
+            <view v-if="title == 4" class="content crm-border-radius" style="overflow: auto;">
+                <text class="con-title">{{ info.title }}</text>
+                <view class="con-time" style="display: flex; justify-content: space-between;">
+                    <text>{{ info.createTime }}</text>
+                    <image src="/static/images/img/acc_logo.png" mode="aspectFit" style="height: 40px; width: auto;" />
+                </view>
+                <view class="my_video" style="width: 100%;">
+                    <!-- 使用 video 组件 -->
+                    <video :src="info.url" controls style="width: 100%;" :show-fullscreen-btn="true"
+                        :enable-play-gesture="true"></video>
+                </view>
+                <text class="con-des">{{ info.description }}</text>
+            </view>
+            <!-- 内容区域 - 视频类型6 -->
+            <view v-if="title == 6" class="content crm-border-radius" style="overflow: auto;">
+                <text class="con-title">{{ info.title }}</text>
+                <text class="con-time">{{ info.subTitle }}</text>
+                <view class="my_video" style="width: 100%;">
+                    <video :src="imgContent" controls style="width: 100%;" :show-fullscreen-btn="true"
+                        :enable-play-gesture="true"></video>
+                </view>
+                <rich-text :nodes="info.content" class="con-des"></rich-text>
+            </view>
+            <!-- 内容区域 - 公告类型7 -->
+            <view v-if="title == 7" class="content crm-border-radius" style="overflow: auto;">
+                <text class="con-title">{{ info.subject }}</text>
+                <rich-text :nodes="info.content" class="con-des"></rich-text>
+            </view>
+            <!-- 内容区域 - 交易观点8 / 财经日历9 -->
+            <view v-if="title == 8 || title == 9" class="content crm-border-radius">
+                <view style="width: 100%; min-height: 800px;">
+                    <!-- #ifdef H5 -->
+                    <iframe width="100%" height="100%" style="min-height: 800px; border: none;" :src="imgContent" />
+                    <!-- #endif -->
+                    <!-- #ifndef H5 -->
+                    <web-view :src="imgContent"></web-view>
+                    <!-- #endif -->
+                </view>
+            </view>
+
+            <!-- 内容区域 - 电子书10 -->
+            <view v-if="title == 10" class="content crm-border-radius" style="overflow: auto;">
+                <view class="ebookBox">
+                    <image :src="imgUrl + info.coverImage" mode="aspectFill" />
+                    <view>
+                        <text class="news-title">{{ info.title }}</text>
+                        <rich-text :nodes="info.content" class="con-des"></rich-text>
+                        <view class="news-status">
+                            <!-- #ifdef H5 -->
+                            <a :href="imgUrl + info.bookUrl" target="_blank">{{ t('blockchain.item12') }}</a>
+                            <!-- #endif -->
+                            <!-- #ifndef H5 -->
+                            <button @click="downloadEbook" class="download-btn">{{ t('blockchain.item12') }}</button>
+                            <!-- #endif -->
+                        </view>
+                    </view>
+                </view>
+            </view>
+
+            <!-- 内容区域 - 视频类型11 -->
+            <view v-if="title == 11" class="content crm-border-radius">
+                <view style="display: flex; justify-content: flex-end; margin-bottom: 15px;">
+                    <image src="/static/images/img/acc_logo.png" mode="aspectFit" style="height: 40px; width: auto;" />
+                </view>
+                <view style="width: 80%; min-height: 800px; margin: auto;">
+                    <view style="position: relative; overflow: hidden; padding-bottom: 56.25%;">
+                        <!-- #ifdef H5 -->
+                        <iframe :src="videoSrc" width="100%" height="100%" frameborder="0" scrolling="auto"
+                            style="position: absolute;" allowfullscreen></iframe>
+                        <!-- #endif -->
+                        <!-- #ifndef H5 -->
+                        <web-view :src="videoSrc"></web-view>
+                        <!-- #endif -->
+                    </view>
+                </view>
+            </view>
+        </view>
+    </cwg-page-wrapper>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, watch } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { useI18n } from 'vue-i18n'
+import { newsApi } from '@/service/news';
+import Config from '@/config/index'
+
+const { t } = useI18n()
+const { Code, Host80 } = Config
+const titleMap = computed(() => ({
+    1: t('News.ViewAnalysis'),
+    2: t('News.NewsInformation'),
+    3: t('News.Announcement'),
+    4: t('News.VideoCommentary'),
+    5: t('News.NewsInformation'),
+    6: t('News.VideoCommentary'),
+    7: t('News.Notice'),
+    8: t('News.TradeIdeas'),
+    9: t('News.FinancialCalendar'),
+    10: t('News.Ebook'),
+    11: t('News.VideoCommentary')
+}));
+// 路由参数
+const title = ref(null)
+const Id = ref(null)
+
+// 数据
+const Content = ref('')
+const imgContent = ref('')
+const imgContentIf = ref(false)
+const info = ref({})
+const imgUrl = Host80
+
+// 视频源计算属性
+const videoSrc = computed(() => {
+    const lang = uni.getStorageSync('lang') || 'en'
+    const isChinese = ['cn', 'zhHant'].includes(lang)
+    return isChinese
+        ? 'https://videos.tradingcentral.cn/players/H5quTuut-iodula4l.html'
+        : 'https://videos.tradingcentral.cn/players/SHILp3nA-iodula4l.html'
+})
+
+// 预览图片
+const previewImage = () => {
+    if (imgContent.value) {
+        uni.previewImage({
+            urls: [imgContent.value],
+            current: 0
+        })
+    }
+}
+
+// 下载电子书
+const downloadEbook = () => {
+    const url = imgUrl + info.value.bookUrl
+    // #ifdef APP-PLUS
+    plus.runtime.openURL(url)
+    // #endif
+    // #ifdef MP-WEIXIN
+    uni.downloadFile({
+        url: url,
+        success: (res) => {
+            if (res.statusCode === 200) {
+                uni.openDocument({
+                    filePath: res.tempFilePath,
+                    success: () => {
+                        uni.showToast({
+                            title: t('common.success'),
+                            icon: 'success'
+                        })
+                    }
+                })
+            }
+        }
+    })
+    // #endif
+}
+
+// 获取新闻详情
+const getNewsSingle = async () => {
+    uni.showLoading({ title: t('common.loading') })
+
+    try {
+        if (title.value == 1) {
+            const res = await newsApi.newsAnalysisSingle({ id: Id.value })
+            if (res.code == 200) {
+                if (res.data) {
+                    imgContent.value = res.data.media
+                    Content.value = res.data.content
+                    imgContentIf.value = !!res.data.media
+                }
+            } else {
+                uni.showToast({ title: res.msg, icon: 'none' })
+            }
+        } else if (title.value == 3) {
+            const res = await newsApi.newsInformationSingle({ id: Id.value })
+            if (res.code == 200) {
+                if (res.data) {
+                    imgContent.value = Host80 + res.data.coverImage
+                    Content.value = res.data.content
+                    imgContentIf.value = !!res.data.coverImage
+                }
+            } else {
+                uni.showToast({ title: res.msg, icon: 'none' })
+            }
+        } else if (title.value == 7) {
+            const res = await newsApi.newsNoticeSingle({ id: Id.value })
+            if (res.code == 200) {
+                if (res.data) {
+                    info.value = res.data
+                }
+            } else {
+                uni.showToast({ title: res.msg, icon: 'none' })
+            }
+        } else if (title.value == 4) {
+            const res = await newsApi.newsWebTvSearchSingle({ id: Id.value })
+            if (res.code == 200) {
+                if (res.data) {
+                    info.value = res.data
+                }
+            } else {
+                uni.showToast({ title: res.msg, icon: 'none' })
+            }
+        } else if (title.value == 5) {
+            const res = await newsApi.newsInformationNewsletterSingle({ id: Id.value })
+            if (res.code == 200) {
+                if (res.data) {
+                    imgContent.value = Host80 + res.data.coverImage
+                    Content.value = res.data.content
+                    imgContentIf.value = !!res.data.coverImage
+                }
+            } else {
+                uni.showToast({ title: res.msg, icon: 'none' })
+            }
+        } else if (title.value == 6) {
+            const res = await newsApi.newsVideoSingle({ id: Id.value })
+            if (res.code == 200) {
+                if (res.data) {
+                    info.value = res.data
+                    imgContent.value = res.data.videoUrl.indexOf('http') > -1
+                        ? res.data.videoUrl
+                        : (Host80 + res.data.videoUrl)
+                }
+            } else {
+                uni.showToast({ title: res.msg, icon: 'none' })
+            }
+        } else if (title.value == 8) {
+            const res = await newsApi.handShakeGet({})
+            if (res.code == 200) {
+                imgContent.value = res.msg
+            } else {
+                uni.showToast({ title: res.msg, icon: 'none' })
+            }
+        } else if (title.value == 9) {
+            const res = await newsApi.handFinancialCalendar({})
+            if (res.code == 200) {
+                imgContent.value = res.msg
+            } else {
+                uni.showToast({ title: res.msg, icon: 'none' })
+            }
+        } else if (title.value == 10) {
+            const res = await newsApi.newsEbookSingle({ id: Id.value })
+            if (res.code == 200) {
+                info.value = res.data
+            } else {
+                uni.showToast({ title: res.msg, icon: 'none' })
+            }
+        }
+    } catch (error) {
+        console.error('加载失败:', error)
+        uni.showToast({ title: t('common.error'), icon: 'none' })
+    } finally {
+        uni.hideLoading()
+    }
+}
+
+// 返回
+const backIndex = () => {
+    uni.navigateTo({
+        url: '/pages/mine/notice'
+    })
+}
+
+// 监听路由参数
+onLoad((query) => {
+    title.value = Number(query.title)
+    Id.value = Number(query.id)
+    getNewsSingle()
+})
+
+// 监听路由变化
+watch([() => title.value, () => Id.value], () => {
+    getNewsSingle()
+})
+</script>
+
+<style scoped lang="scss">
+.content {
+    flex: 1;
+    width: 100%;
+    background-color: #fff;
+    overflow: hidden;
+    overflow-y: auto;
+    text-align: left;
+    padding: 30rpx;
+    box-sizing: border-box;
+    line-height: 1.8;
+
+    .img {
+        margin-bottom: 30rpx;
+
+        .content-image {
+            width: 100%;
+            max-height: 400rpx;
+            object-fit: contain;
+        }
+    }
+
+    .con-title {
+        font-size: 36rpx;
+        font-weight: bold;
+        margin: 20rpx 0;
+        color: #333;
+    }
+
+    .con-time {
+        margin-bottom: 30rpx;
+        font-size: 28rpx;
+        color: #999;
+    }
+
+    .con-des {
+        margin: 30rpx 0;
+        font-size: 28rpx;
+        color: #666;
+    }
+}
+
+.ebookBox {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+
+    @media (min-width: 768px) {
+        flex-direction: row;
+        align-items: flex-start;
+    }
+
+    image {
+        width: 360rpx;
+        height: 525rpx;
+        margin-right: 50rpx;
+        border-radius: 16rpx;
+        object-fit: cover;
+    }
+
+    .news-title {
+        color: #EB3F57;
+        font-size: 36rpx;
+        font-weight: bold;
+        margin-bottom: 20rpx;
+    }
+
+    .news-status {
+        margin-top: 30rpx;
+
+        a,
+        .download-btn {
+            display: inline-block;
+            color: #ffffff;
+            height: 60rpx;
+            line-height: 60rpx;
+            background-color: #EB3F57;
+            padding: 0 30rpx;
+            font-weight: bold;
+            border-radius: 8rpx;
+            font-size: 28rpx;
+        }
+    }
+}
+
+// 富文本样式
+:deep(.rich-text) {
+    font-size: 28rpx;
+    color: #666;
+
+    table {
+        border-collapse: collapse;
+        width: 100%;
+        margin: 20rpx 0;
+    }
+
+    th,
+    td {
+        border: 1rpx solid #dcdfe6;
+        padding: 16rpx;
+        text-align: left;
+    }
+
+    th {
+        background-color: #f5f7fa;
+    }
+
+    img {
+        max-width: 100%;
+        height: auto;
+    }
+}
+
+.content-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-size: px2rpx(20);
+    font-weight: 500;
+
+    .content-title-btns {
+        margin: px2rpx(8) 0;
+
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: px2rpx(12);
+
+        .btn-primary {
+            min-width: px2rpx(120);
+            background-color: var(--color-error);
+            color: white;
+            padding: 0 px2rpx(12);
+            border: none;
+            font-size: px2rpx(14);
+            text-align: center;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            gap: px2rpx(8);
+        }
+
+        .btn-primary:active {
+            background-color: var(--color-navy-700);
+        }
+    }
+}
+</style>

+ 150 - 0
pages/mine/notice.vue

@@ -0,0 +1,150 @@
+<template>
+    <cwg-page-wrapper class="create-page" :isHeaderFixed="true">
+        <view class="info-card">
+            <view class="content-title">
+                <view v-t="'News.Notice'"></view>
+            </view>
+            <view class="search-bar">
+                <cwg-combox v-model:value="search.read" :options="typeMap" :placeholder="t('placeholder.choose')" />
+            </view>
+            <cwg-tabel ref="tableRef" :columns="columns" :queryParams="search" :api="listApi" :show-operation="false"
+                :showPagination="false">
+                <template #subject="{ row }">
+                    <view class="subject underline" @click="toView(row, 7)">
+                        {{ row.subject }}
+                    </view>
+                </template>
+            </cwg-tabel>
+        </view>
+    </cwg-page-wrapper>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, onMounted } from 'vue';
+import { useI18n } from 'vue-i18n';
+const { t, locale } = useI18n();
+import useRouter from '@/hooks/useRouter'
+const router = useRouter()
+import { newsApi } from '@/service/news';
+const search = ref({})
+const typeMap = computed(() => ([
+    { value: null, text: t('State.All') },
+    { value: 0, text: t('News.Unread') },
+    { value: 1, text: t('News.Read') }
+]));
+
+const isZh = computed(() => ['cn', 'zh', 'zhHant'].includes(locale.value));
+// 表格列配置
+const columns = ref([
+    {
+        prop: 'subject',
+        label: t('News.Title'),
+        align: 'left',
+        slot: 'subject'
+    },
+    {
+        prop: 'addTime',
+        label: t('Drawer.Label.Date'),
+        type: 'date',
+        dateFormat: 'YYYY-MM-DD HH:mm',
+        align: 'left'
+    },
+    {
+        prop: 'status',
+        label: t('Custom.Recording.Status'),
+        formatter: ({ row }) => row.read ? t('News.Read') : t('News.Unread'),
+        align: 'left'
+    }
+])
+
+
+//查看详情
+const toView = (data, title) => {
+    router.push({ path: "/pages/mine/new", query: { title, id: data.id } }).catch(err => err);
+    if (title == 7) {
+        data.read = 1;
+    }
+}
+const addFileDialog = ref(null);
+const listApi = ref(null)
+listApi.value = newsApi.newsNoticeList
+</script>
+
+<style scoped lang="scss">
+@import "@/uni.scss";
+
+.avatar {
+    width: px2rpx(60);
+    height: px2rpx(60);
+    border-radius: 4px;
+}
+
+.content-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-size: px2rpx(20);
+    font-weight: 500;
+
+    .content-title-btns {
+        margin: px2rpx(8) 0;
+
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: px2rpx(12);
+
+        .btn-primary {
+            min-width: px2rpx(120);
+            background-color: var(--color-error);
+            color: white;
+            padding: 0 px2rpx(12);
+            border: none;
+            font-size: px2rpx(14);
+            text-align: center;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            gap: px2rpx(8);
+        }
+
+        .btn-primary:active {
+            background-color: var(--color-navy-700);
+        }
+    }
+}
+
+.operation-btn {
+    :deep(span) {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: px2rpx(4);
+        cursor: pointer;
+        background-color: var(--color-slate-150);
+        padding: px2rpx(8) 0;
+    }
+}
+
+.operation-btn.disabled {
+    cursor: not-allowed;
+    opacity: 0.5;
+}
+
+.search-bar {
+    display: flex;
+    align-items: center;
+    justify-content: flex-start;
+    flex-wrap: wrap;
+    gap: px2rpx(16);
+    margin: px2rpx(16) 0;
+
+    .cwg-combox,
+    .uni-easyinput,
+    .uni-date {
+        width: px2rpx(240) !important;
+        flex: none;
+    }
+}
+</style>

+ 1 - 0
static/icons/crm-angle-left.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M201.4 297.4C188.9 309.9 188.9 330.2 201.4 342.7L361.4 502.7C373.9 515.2 394.2 515.2 406.7 502.7C419.2 490.2 419.2 469.9 406.7 457.4L269.3 320L406.6 182.6C419.1 170.1 419.1 149.8 406.6 137.3C394.1 124.8 373.8 124.8 361.3 137.3L201.3 297.3z"/></svg>

+ 1 - 0
static/icons/crm-bars-staggered.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M64 160C64 142.3 78.3 128 96 128L480 128C497.7 128 512 142.3 512 160C512 177.7 497.7 192 480 192L96 192C78.3 192 64 177.7 64 160zM128 320C128 302.3 142.3 288 160 288L544 288C561.7 288 576 302.3 576 320C576 337.7 561.7 352 544 352L160 352C142.3 352 128 337.7 128 320zM512 480C512 497.7 497.7 512 480 512L96 512C78.3 512 64 497.7 64 480C64 462.3 78.3 448 96 448L480 448C497.7 448 512 462.3 512 480z"/></svg>

+ 1 - 0
static/icons/crm-building-columns.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M335.9 84.2C326.1 78.6 314 78.6 304.1 84.2L80.1 212.2C67.5 219.4 61.3 234.2 65 248.2C68.7 262.2 81.5 272 96 272L128 272L128 480L128 480L76.8 518.4C68.7 524.4 64 533.9 64 544C64 561.7 78.3 576 96 576L544 576C561.7 576 576 561.7 576 544C576 533.9 571.3 524.4 563.2 518.4L512 480L512 272L544 272C558.5 272 571.2 262.2 574.9 248.2C578.6 234.2 572.4 219.4 559.8 212.2L335.8 84.2zM464 272L464 480L400 480L400 272L464 272zM352 272L352 480L288 480L288 272L352 272zM240 272L240 480L176 480L176 272L240 272zM320 160C337.7 160 352 174.3 352 192C352 209.7 337.7 224 320 224C302.3 224 288 209.7 288 192C288 174.3 302.3 160 320 160z"/></svg>

+ 1 - 0
static/icons/crm-chart-area.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M96 96C113.7 96 128 110.3 128 128L128 464C128 472.8 135.2 480 144 480L544 480C561.7 480 576 494.3 576 512C576 529.7 561.7 544 544 544L144 544C99.8 544 64 508.2 64 464L64 128C64 110.3 78.3 96 96 96zM304 160C310.7 160 317.1 162.8 321.7 167.8L392.8 245.3L439 199C448.4 189.6 463.6 189.6 472.9 199L536.9 263C541.4 267.5 543.9 273.6 543.9 280L543.9 392C543.9 405.3 533.2 416 519.9 416L215.9 416C202.6 416 191.9 405.3 191.9 392L191.9 280C191.9 274 194.2 268.2 198.2 263.8L286.2 167.8C290.7 162.8 297.2 160 303.9 160z"/></svg>

+ 1 - 0
static/icons/crm-circle-dollar-to-slot.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M128 288C128 182 214 96 320 96C426 96 512 182 512 288C512 394 426 480 320 480C214 480 128 394 128 288zM304 196L304 200C275.2 200.3 252 223.7 252 252.5C252 278.2 270.5 300.1 295.9 304.3L337.6 311.3C343.6 312.3 348 317.5 348 323.6C348 330.5 342.4 336.1 335.5 336.1L280 336C269 336 260 345 260 356C260 367 269 376 280 376L304 376L304 380C304 391 313 400 324 400C335 400 344 391 344 380L344 375.3C369 371.2 388 349.6 388 323.5C388 297.8 369.5 275.9 344.1 271.7L302.4 264.7C296.4 263.7 292 258.5 292 252.4C292 245.5 297.6 239.9 304.5 239.9L352 239.9C363 239.9 372 230.9 372 219.9C372 208.9 363 199.9 352 199.9L344 199.9L344 195.9C344 184.9 335 175.9 324 175.9C313 175.9 304 184.9 304 195.9zM80 408L80 512C80 520.8 87.2 528 96 528L544 528C552.8 528 560 520.8 560 512L560 408C560 394.7 570.7 384 584 384C597.3 384 608 394.7 608 408L608 512C608 547.3 579.3 576 544 576L96 576C60.7 576 32 547.3 32 512L32 408C32 394.7 42.7 384 56 384C69.3 384 80 394.7 80 408z"/></svg>

+ 1 - 0
static/icons/crm-circle-user.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M463 448.2C440.9 409.8 399.4 384 352 384L288 384C240.6 384 199.1 409.8 177 448.2C212.2 487.4 263.2 512 320 512C376.8 512 427.8 487.3 463 448.2zM64 320C64 178.6 178.6 64 320 64C461.4 64 576 178.6 576 320C576 461.4 461.4 576 320 576C178.6 576 64 461.4 64 320zM320 336C359.8 336 392 303.8 392 264C392 224.2 359.8 192 320 192C280.2 192 248 224.2 248 264C248 303.8 280.2 336 320 336z"/></svg>

+ 1 - 0
static/icons/crm-clock-rotate-left.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M320 128C426 128 512 214 512 320C512 426 426 512 320 512C254.8 512 197.1 479.5 162.4 429.7C152.3 415.2 132.3 411.7 117.8 421.8C103.3 431.9 99.8 451.9 109.9 466.4C156.1 532.6 233 576 320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C234.3 64 158.5 106.1 112 170.7L112 144C112 126.3 97.7 112 80 112C62.3 112 48 126.3 48 144L48 256C48 273.7 62.3 288 80 288L104.6 288C105.1 288 105.6 288 106.1 288L192.1 288C209.8 288 224.1 273.7 224.1 256C224.1 238.3 209.8 224 192.1 224L153.8 224C186.9 166.6 249 128 320 128zM344 216C344 202.7 333.3 192 320 192C306.7 192 296 202.7 296 216L296 320C296 326.4 298.5 332.5 303 337L375 409C384.4 418.4 399.6 418.4 408.9 409C418.2 399.6 418.3 384.4 408.9 375.1L343.9 310.1L343.9 216z"/></svg>

+ 1 - 0
static/icons/crm-credit-card.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M64 192L64 224L576 224L576 192C576 156.7 547.3 128 512 128L128 128C92.7 128 64 156.7 64 192zM64 272L64 448C64 483.3 92.7 512 128 512L512 512C547.3 512 576 483.3 576 448L576 272L64 272zM128 424C128 410.7 138.7 400 152 400L200 400C213.3 400 224 410.7 224 424C224 437.3 213.3 448 200 448L152 448C138.7 448 128 437.3 128 424zM272 424C272 410.7 282.7 400 296 400L360 400C373.3 400 384 410.7 384 424C384 437.3 373.3 448 360 448L296 448C282.7 448 272 437.3 272 424z"/></svg>

+ 1 - 0
static/icons/crm-diagram.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M64 144C64 117.5 85.5 96 112 96L208 96C234.5 96 256 117.5 256 144L256 160L384 160L384 144C384 117.5 405.5 96 432 96L528 96C554.5 96 576 117.5 576 144L576 240C576 266.5 554.5 288 528 288L432 288C405.5 288 384 266.5 384 240L384 224L256 224L256 240C256 247.3 254.3 254.3 251.4 260.5L320 352L400 352C426.5 352 448 373.5 448 400L448 496C448 522.5 426.5 544 400 544L304 544C277.5 544 256 522.5 256 496L256 400C256 392.7 257.7 385.7 260.6 379.5L192 288L112 288C85.5 288 64 266.5 64 240L64 144z"/></svg>

+ 1 - 0
static/icons/crm-download.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M352 96C352 78.3 337.7 64 320 64C302.3 64 288 78.3 288 96L288 306.7L246.6 265.3C234.1 252.8 213.8 252.8 201.3 265.3C188.8 277.8 188.8 298.1 201.3 310.6L297.3 406.6C309.8 419.1 330.1 419.1 342.6 406.6L438.6 310.6C451.1 298.1 451.1 277.8 438.6 265.3C426.1 252.8 405.8 252.8 393.3 265.3L352 306.7L352 96zM160 384C124.7 384 96 412.7 96 448L96 480C96 515.3 124.7 544 160 544L480 544C515.3 544 544 515.3 544 480L544 448C544 412.7 515.3 384 480 384L433.1 384L376.5 440.6C345.3 471.8 294.6 471.8 263.4 440.6L206.9 384L160 384zM464 440C477.3 440 488 450.7 488 464C488 477.3 477.3 488 464 488C450.7 488 440 477.3 440 464C440 450.7 450.7 440 464 440z"/></svg>

+ 1 - 0
static/icons/crm-file.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M192 64C156.7 64 128 92.7 128 128L128 512C128 547.3 156.7 576 192 576L448 576C483.3 576 512 547.3 512 512L512 234.5C512 217.5 505.3 201.2 493.3 189.2L386.7 82.7C374.7 70.7 358.5 64 341.5 64L192 64zM453.5 240L360 240C346.7 240 336 229.3 336 216L336 122.5L453.5 240z"/></svg>

+ 1 - 0
static/icons/crm-headset.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M320 128C241 128 175.3 185.3 162.3 260.7C171.6 257.7 181.6 256 192 256L208 256C234.5 256 256 277.5 256 304L256 400C256 426.5 234.5 448 208 448L192 448C139 448 96 405 96 352L96 288C96 164.3 196.3 64 320 64C443.7 64 544 164.3 544 288L544 456.1C544 522.4 490.2 576.1 423.9 576.1L336 576L304 576C277.5 576 256 554.5 256 528C256 501.5 277.5 480 304 480L336 480C362.5 480 384 501.5 384 528L384 528L424 528C463.8 528 496 495.8 496 456L496 435.1C481.9 443.3 465.5 447.9 448 447.9L432 447.9C405.5 447.9 384 426.4 384 399.9L384 303.9C384 277.4 405.5 255.9 432 255.9L448 255.9C458.4 255.9 468.3 257.5 477.7 260.6C464.7 185.3 399.1 127.9 320 127.9z"/></svg>

+ 1 - 0
static/icons/crm-house.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M341.8 72.6C329.5 61.2 310.5 61.2 298.3 72.6L74.3 280.6C64.7 289.6 61.5 303.5 66.3 315.7C71.1 327.9 82.8 336 96 336L112 336L112 512C112 547.3 140.7 576 176 576L464 576C499.3 576 528 547.3 528 512L528 336L544 336C557.2 336 569 327.9 573.8 315.7C578.6 303.5 575.4 289.5 565.8 280.6L341.8 72.6zM304 384L336 384C362.5 384 384 405.5 384 432L384 528L256 528L256 432C256 405.5 277.5 384 304 384z"/></svg>

+ 1 - 0
static/icons/crm-image.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M160 96C124.7 96 96 124.7 96 160L96 480C96 515.3 124.7 544 160 544L480 544C515.3 544 544 515.3 544 480L544 160C544 124.7 515.3 96 480 96L160 96zM224 176C250.5 176 272 197.5 272 224C272 250.5 250.5 272 224 272C197.5 272 176 250.5 176 224C176 197.5 197.5 176 224 176zM368 288C376.4 288 384.1 292.4 388.5 299.5L476.5 443.5C481 450.9 481.2 460.2 477 467.8C472.8 475.4 464.7 480 456 480L184 480C175.1 480 166.8 475 162.7 467.1C158.6 459.2 159.2 449.6 164.3 442.3L220.3 362.3C224.8 355.9 232.1 352.1 240 352.1C247.9 352.1 255.2 355.9 259.7 362.3L286.1 400.1L347.5 299.6C351.9 292.5 359.6 288.1 368 288.1z"/></svg>

+ 1 - 0
static/icons/crm-leaf.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M535.3 70.7C541.7 64.6 551 62.4 559.6 65.2C569.4 68.5 576 77.7 576 88L576 274.9C576 406.1 467.9 512 337.2 512C260.2 512 193.8 462.5 169.7 393.3C134.3 424.1 112 469.4 112 520C112 533.3 101.3 544 88 544C74.7 544 64 533.3 64 520C64 445.1 102.2 379.1 160.1 340.3C195.4 316.7 237.5 304 280 304L360 304C373.3 304 384 293.3 384 280C384 266.7 373.3 256 360 256L280 256C240.3 256 202.7 264.8 169 280.5C192.3 210.5 258.2 160 336 160C402.4 160 451.8 137.9 484.7 116C503.9 103.2 520.2 87.9 535.4 70.7z"/></svg>

+ 1 - 0
static/icons/crm-lock.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M256 160L256 224L384 224L384 160C384 124.7 355.3 96 320 96C284.7 96 256 124.7 256 160zM192 224L192 160C192 89.3 249.3 32 320 32C390.7 32 448 89.3 448 160L448 224C483.3 224 512 252.7 512 288L512 512C512 547.3 483.3 576 448 576L192 576C156.7 576 128 547.3 128 512L128 288C128 252.7 156.7 224 192 224z"/></svg>

+ 1 - 0
static/icons/crm-menu.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1772593056372" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="32938" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M0 0h1024v1024H0z" fill="#FFFFFF" fill-opacity=".01" p-id="32939"></path><path d="M853.333333 192V256H170.666667V192zM725.333333 490.666667V554.666667H170.666667v-64zM853.333333 789.333333V853.333333H170.666667v-64z" fill="#666666" p-id="32940"></path></svg>

+ 1 - 0
static/icons/crm-photo-film.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M192 128C192 92.7 220.7 64 256 64L576 64C611.3 64 640 92.7 640 128L640 352C640 387.3 611.3 416 576 416L256 416C220.7 416 192 387.3 192 352L192 128zM320 160C320 142.3 305.7 128 288 128C270.3 128 256 142.3 256 160C256 177.7 270.3 192 288 192C305.7 192 320 177.7 320 160zM476.5 171.5C472.1 164.4 464.4 160 456 160C447.6 160 439.9 164.4 435.5 171.5L381.5 259.8L363.6 234.2C359.1 227.8 351.8 224 343.9 224C336 224 328.7 227.8 324.2 234.2L268.2 314.2C263.1 321.5 262.4 331.1 266.6 339C270.8 346.9 279.1 352 288 352L544 352C552.7 352 560.7 347.3 564.9 339.7C569.1 332.1 569 322.9 564.4 315.4L476.4 171.4zM144 192L144 352C144 413.9 194.1 464 256 464L448 464L448 480C448 515.3 419.3 544 384 544L64 544C28.7 544 0 515.3 0 480L0 256C0 220.7 28.7 192 64 192L144 192zM52 260L52 284C52 292.8 59.2 300 68 300L92 300C100.8 300 108 292.8 108 284L108 260C108 251.2 100.8 244 92 244L68 244C59.2 244 52 251.2 52 260zM68 340C59.2 340 52 347.2 52 356L52 380C52 388.8 59.2 396 68 396L92 396C100.8 396 108 388.8 108 380L108 356C108 347.2 100.8 340 92 340L68 340zM68 436C59.2 436 52 443.2 52 452L52 476C52 484.8 59.2 492 68 492L92 492C100.8 492 108 484.8 108 476L108 452C108 443.2 100.8 436 92 436L68 436z"/></svg>

+ 1 - 0
static/icons/crm-plus.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M352 128C352 110.3 337.7 96 320 96C302.3 96 288 110.3 288 128L288 288L128 288C110.3 288 96 302.3 96 320C96 337.7 110.3 352 128 352L288 352L288 512C288 529.7 302.3 544 320 544C337.7 544 352 529.7 352 512L352 352L512 352C529.7 352 544 337.7 544 320C544 302.3 529.7 288 512 288L352 288L352 128z"/></svg>

+ 1 - 0
static/icons/crm-star.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M341.5 45.1C337.4 37.1 329.1 32 320.1 32C311.1 32 302.8 37.1 298.7 45.1L225.1 189.3L65.2 214.7C56.3 216.1 48.9 222.4 46.1 231C43.3 239.6 45.6 249 51.9 255.4L166.3 369.9L141.1 529.8C139.7 538.7 143.4 547.7 150.7 553C158 558.3 167.6 559.1 175.7 555L320.1 481.6L464.4 555C472.4 559.1 482.1 558.3 489.4 553C496.7 547.7 500.4 538.8 499 529.8L473.7 369.9L588.1 255.4C594.5 249 596.7 239.6 593.9 231C591.1 222.4 583.8 216.1 574.8 214.7L415 189.3L341.5 45.1z"/></svg>

+ 1 - 0
static/icons/crm-trash-can.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M232.7 69.9C237.1 56.8 249.3 48 263.1 48L377 48C390.8 48 403 56.8 407.4 69.9L416 96L512 96C529.7 96 544 110.3 544 128C544 145.7 529.7 160 512 160L128 160C110.3 160 96 145.7 96 128C96 110.3 110.3 96 128 96L224 96L232.7 69.9zM128 208L512 208L512 512C512 547.3 483.3 576 448 576L192 576C156.7 576 128 547.3 128 512L128 208zM216 272C202.7 272 192 282.7 192 296L192 488C192 501.3 202.7 512 216 512C229.3 512 240 501.3 240 488L240 296C240 282.7 229.3 272 216 272zM320 272C306.7 272 296 282.7 296 296L296 488C296 501.3 306.7 512 320 512C333.3 512 344 501.3 344 488L344 296C344 282.7 333.3 272 320 272zM424 272C410.7 272 400 282.7 400 296L400 488C400 501.3 410.7 512 424 512C437.3 512 448 501.3 448 488L448 296C448 282.7 437.3 272 424 272z"/></svg>

+ 1 - 0
static/icons/crm-user-pen.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M256.1 312C322.4 312 376.1 258.3 376.1 192C376.1 125.7 322.4 72 256.1 72C189.8 72 136.1 125.7 136.1 192C136.1 258.3 189.8 312 256.1 312zM226.4 368C127.9 368 48.1 447.8 48.1 546.3C48.1 562.7 61.4 576 77.8 576L274.3 576L285.2 521.5C289.5 499.8 300.2 479.9 315.8 464.3L383.1 397C355.1 378.7 321.7 368.1 285.7 368.1L226.3 368.1zM332.3 530.9L320.4 590.5C320.2 591.4 320.1 592.4 320.1 593.4C320.1 601.4 326.6 608 334.7 608C335.7 608 336.6 607.9 337.6 607.7L397.2 595.8C409.6 593.3 421 587.2 429.9 578.3L548.8 459.4L468.8 379.4L349.9 498.3C341 507.2 334.9 518.6 332.4 531zM600.1 407.9C622.2 385.8 622.2 350 600.1 327.9C578 305.8 542.2 305.8 520.1 327.9L491.3 356.7L571.3 436.7L600.1 407.9z"/></svg>

+ 1 - 0
static/icons/crm-user.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M320 312C386.3 312 440 258.3 440 192C440 125.7 386.3 72 320 72C253.7 72 200 125.7 200 192C200 258.3 253.7 312 320 312zM290.3 368C191.8 368 112 447.8 112 546.3C112 562.7 125.3 576 141.7 576L498.3 576C514.7 576 528 562.7 528 546.3C528 447.8 448.2 368 349.7 368L290.3 368z"/></svg>

+ 1 - 0
static/icons/crm-users.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M320 80C377.4 80 424 126.6 424 184C424 241.4 377.4 288 320 288C262.6 288 216 241.4 216 184C216 126.6 262.6 80 320 80zM96 152C135.8 152 168 184.2 168 224C168 263.8 135.8 296 96 296C56.2 296 24 263.8 24 224C24 184.2 56.2 152 96 152zM0 480C0 409.3 57.3 352 128 352C140.8 352 153.2 353.9 164.9 357.4C132 394.2 112 442.8 112 496L112 512C112 523.4 114.4 534.2 118.7 544L32 544C14.3 544 0 529.7 0 512L0 480zM521.3 544C525.6 534.2 528 523.4 528 512L528 496C528 442.8 508 394.2 475.1 357.4C486.8 353.9 499.2 352 512 352C582.7 352 640 409.3 640 480L640 512C640 529.7 625.7 544 608 544L521.3 544zM472 224C472 184.2 504.2 152 544 152C583.8 152 616 184.2 616 224C616 263.8 583.8 296 544 296C504.2 296 472 263.8 472 224zM160 496C160 407.6 231.6 336 320 336C408.4 336 480 407.6 480 496L480 512C480 529.7 465.7 544 448 544L192 544C174.3 544 160 529.7 160 512L160 496z"/></svg>

+ 1 - 0
static/icons/crm-wallet.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M128 96C92.7 96 64 124.7 64 160L64 448C64 483.3 92.7 512 128 512L512 512C547.3 512 576 483.3 576 448L576 256C576 220.7 547.3 192 512 192L136 192C122.7 192 112 181.3 112 168C112 154.7 122.7 144 136 144L520 144C533.3 144 544 133.3 544 120C544 106.7 533.3 96 520 96L128 96zM480 320C497.7 320 512 334.3 512 352C512 369.7 497.7 384 480 384C462.3 384 448 369.7 448 352C448 334.3 462.3 320 480 320z"/></svg>

BIN
static/images/img/Notifications.png


BIN
static/images/img/acc_logo.png


BIN
static/images/img/account-reg-1.png


BIN
static/images/img/account-reg-2.png


BIN
static/images/img/account-reg-3.png


BIN
static/images/img/applicaiton-history.png


BIN
static/images/img/bank-info.png


BIN
static/images/img/dashboard.png


BIN
static/images/img/file-management.png


BIN
static/images/img/login.png


BIN
static/images/img/payment-history.png


BIN
static/images/img/personal-info.png


BIN
static/images/img/promotions.png


BIN
static/images/img/registration.png


BIN
static/images/img/security-center.png


BIN
static/images/img/sidebar-menu.png


BIN
static/images/info/bank-information-1.webp


BIN
static/images/info/bank-information-2.webp


BIN
static/images/info/bank-information-3.webp


BIN
static/images/info/success.png


BIN
static/images/trading/android.png


BIN
static/images/trading/apple.png


BIN
static/images/trading/icone-windows-ios.png


BIN
static/images/trading/icone-windows.png


BIN
static/images/trading/ios.png


BIN
static/images/trading/trading-instruments-1.png


BIN
static/images/trading/trading-instruments-2.png


BIN
static/images/trading/trading-instruments-3.png


BIN
static/images/trading/trading-instruments-4.png


BIN
static/images/trading/trading-instruments-6.png


BIN
static/images/trading/trading-instruments-7.png


Plik diff jest za duży
+ 0 - 0
static/js/jsvm_all.js


+ 379 - 39
static/scss/global/global.scss

@@ -5,21 +5,48 @@
 }
 
 :root {
-    --primary-color: #ea002a;
-    --success-color: #4CD964;
-    --warning-color: #F0AD4E;
-    --error-color: #DD524D;
-    --main-bg: #fff;
-    --card-bg: #222;
-    --action-bg: #fff;
-    --main-yellow: #ea002a;
-    --lable: #454745;
-    --main-yellow-dark: rgb(15 120 71);
-    --white: #000;
-    --gray: #aaa;
-    --border: #868685;
-    --black: #fff;
-    --black1: #343434;
+    --color-red-400: oklch(70.4% .191 22.216);
+    --color-orange-600: oklch(64.6% .222 41.116);
+    --color-amber-50: oklch(98.7% .022 95.277);
+    --color-amber-400: oklch(82.8% .189 84.429);
+    --color-yellow-100: oklch(97.3% .071 103.193);
+    --color-yellow-400: oklch(85.2% .199 91.936);
+    --color-yellow-500: oklch(79.5% .184 86.047);
+    --color-green-300: oklch(87.1% .15 154.449);
+    --color-green-400: oklch(79.2% .209 151.711);
+    --color-sky-100: oklch(95.1% .026 236.824);
+    --color-sky-400: oklch(74.6% .16 232.661);
+    --color-blue-500: oklch(62.3% .214 259.815);
+    --color-blue-600: oklch(54.6% .245 262.881);
+    --color-indigo-50: oklch(96.2% .018 272.314);
+    --color-indigo-100: oklch(93% .034 272.788);
+    --color-indigo-400: oklch(67.3% .182 276.935);
+    --color-indigo-600: oklch(51.1% .262 276.966);
+    --color-violet-400: oklch(70.2% .183 293.541);
+    --color-purple-300: oklch(82.7% .119 306.383);
+    --color-purple-500: oklch(62.7% .265 303.9);
+    --color-purple-600: oklch(55.8% .288 302.321);
+    --color-fuchsia-400: oklch(74% .238 322.16);
+    --color-fuchsia-600: oklch(59.1% .293 322.896);
+    --color-pink-100: oklch(94.8% .028 342.258);
+    --color-pink-300: oklch(82.3% .12 346.018);
+    --color-pink-500: oklch(0.59 0.22 25.62);
+    --color-pink-600: oklch(0.67 0.21 36.35);
+    --color-rose-500: oklch(64.5% .246 16.439);
+    --color-slate-50: oklch(98.4% .003 247.858);
+    --color-slate-100: oklch(96.8% .007 247.896);
+    --color-slate-200: oklch(92.9% .013 255.508);
+    --color-slate-300: oklch(86.9% .022 252.894);
+    --color-slate-400: oklch(70.4% .04 256.788);
+    --color-slate-500: oklch(55.4% .046 257.417);
+    --color-slate-600: oklch(44.6% .043 257.281);
+    --color-slate-700: oklch(37.2% .044 257.287);
+    --color-slate-800: oklch(27.9% .041 260.031);
+    --color-slate-900: oklch(20.8% .042 265.755);
+    --color-gray-200: oklch(92.8% .006 264.531);
+    --color-zinc-100: oklch(96.7% .001 286.375);
+    --color-black: #000;
+    --color-white: #fff;
     --font-size-10: px2rpx(10);
     --font-size-12: px2rpx(12);
     --font-size-13: px2rpx(13);
@@ -36,23 +63,125 @@
     --font-size-36: px2rpx(36);
     --font-size-40: px2rpx(40);
     --el-component-size: px2rpx(40);
+    --ease-in: cubic-bezier(.4, 0, 1, 1);
+    --ease-out: cubic-bezier(0, 0, .2, 1);
+    --ease-in-out: cubic-bezier(.4, 0, .2, 1);
+    --animate-spin: spin 1s linear infinite;
+    --animate-ping: ping 1s cubic-bezier(0, 0, .2, 1) infinite;
+    --animate-pulse: pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;
+    --blur-sm: 8px;
+    --default-transition-duration: .15s;
+    --default-transition-timing-function: cubic-bezier(.4, 0, .2, 1);
+    --default-font-family: var(--font-sans);
+    --default-mono-font-family: var(--font-mono);
+    --font-inter: Inter, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+    --color-navy-50: #e7e9ef;
+    --color-navy-100: #fff;
+    --color-navy-200: #a3adc2;
+    --color-navy-300: #697a9b;
+    --color-navy-400: #5c6b8a;
+    --color-navy-450: #465675;
+    --color-navy-500: #3a3c43;
+    --color-navy-600: #313e59;
+    --color-navy-700: #242726;
+    --color-navy-750: #222e45;
+    --color-navy-800: #202b40;
+    --color-navy-900: #141a18;
+    --color-slate-150: #e9eef5;
+    --color-primary: #4f46e5;
+    --color-primary-focus: #4338ca;
+    --color-secondary-light: #ff57d8;
+    --color-secondary: #141a18;
+    --color-secondary-focus: #242726;
+    --color-accent-light: #242726;
+    --color-accent: #5f5af6;
+    --color-accent-focus: #4d47f5;
+    --color-info: #0ea5e9;
+    --color-info-focus: #0284c7;
+    --color-success: #1ac23a;
+    --color-success-focus: #17a732;
+    --color-warning: #ff9800;
+    --color-warning-focus: #e68200;
+    --color-error: #e32326;
+    --color-error-focus: #f03000;
+    --text-tiny: .625rem;
+    --text-tiny--line-height: .8125rem;
+    --text-tiny-plus: .6875rem;
+    --text-tiny-plus--line-height: .875rem;
+    --text-xs-plus: .8125rem;
+    --text-xs-plus--line-height: 1.125rem;
+    --text-sm-plus: .9375rem;
+    --text-sm-plus--line-height: 1.375rem;
+    --animate-shimmer: shimmer 2s linear infinite;
+    // 状态背景色
+    --status-warning-bg: var(--color-amber-50);
+    --status-success-bg: var(--color-green-300);
+    --status-processing-bg: var(--color-sky-100);
+    --status-danger-bg: var(--color-pink-100);
+    --status-expired-bg: var(--color-slate-200);
+    --status-cancelled-bg: var(--color-slate-100);
+
+    // 状态文字颜色
+    --status-warning-text: var(--color-orange-600);
+    --status-success-text: var(--color-green-400);
+    --status-processing-text: var(--color-blue-600);
+    --status-danger-text: var(--color-rose-500);
+    --status-expired-text: var(--color-slate-600);
+    --status-cancelled-text: var(--color-slate-500);
+
+    // 状态边框颜色
+    --status-warning-border: var(--color-amber-400);
+    --status-success-border: var(--color-green-400);
+    --status-processing-border: var(--color-sky-400);
+    --status-danger-border: var(--color-pink-300);
+    --status-expired-border: var(--color-slate-400);
+    --status-cancelled-border: var(--color-slate-300);
 }
 
 .dark {
-    --primary-color: #1a1a1a;
-    --success-color: #4CD964;
-    --warning-color: #F0AD4E;
-    --error-color: #DD524D;
-    --main-bg: #181a1b;
-    --card-bg: #222;
-    --action-bg: #232323;
-    --main-yellow: #ea002a;
-    --main-yellow-dark: rgb(15 120 71);
-    --white: #fff;
-    --lable: #aaa;
-    --gray: #aaa;
-    --border: #333;
-    --black: #000;
+    --color-red-400: oklch(70.4% .191 22.216);
+    --color-orange-600: oklch(64.6% .222 41.116);
+    --color-amber-50: oklch(98.7% .022 95.277);
+    --color-amber-400: oklch(82.8% .189 84.429);
+    --color-yellow-100: oklch(97.3% .071 103.193);
+    --color-yellow-400: oklch(85.2% .199 91.936);
+    --color-yellow-500: oklch(79.5% .184 86.047);
+    --color-green-300: oklch(87.1% .15 154.449);
+    --color-green-400: oklch(79.2% .209 151.711);
+    --color-sky-100: oklch(95.1% .026 236.824);
+    --color-sky-400: oklch(74.6% .16 232.661);
+    --color-blue-500: oklch(62.3% .214 259.815);
+    --color-blue-600: oklch(54.6% .245 262.881);
+    --color-indigo-50: oklch(96.2% .018 272.314);
+    --color-indigo-100: oklch(93% .034 272.788);
+    --color-indigo-400: oklch(67.3% .182 276.935);
+    --color-indigo-600: oklch(51.1% .262 276.966);
+    --color-violet-400: oklch(70.2% .183 293.541);
+    --color-purple-300: oklch(82.7% .119 306.383);
+    --color-purple-500: oklch(62.7% .265 303.9);
+    --color-purple-600: oklch(55.8% .288 302.321);
+    --color-fuchsia-400: oklch(74% .238 322.16);
+    --color-fuchsia-600: oklch(59.1% .293 322.896);
+    --color-pink-100: oklch(94.8% .028 342.258);
+    --color-pink-300: oklch(82.3% .12 346.018);
+    --color-pink-500: oklch(0.59 0.22 25.62);
+    --color-pink-600: oklch(0.67 0.21 36.35);
+    --color-rose-500: oklch(64.5% .246 16.439);
+    --color-slate-50: oklch(98.4% .003 247.858);
+    --color-slate-100: oklch(96.8% .007 247.896);
+    --color-slate-200: oklch(92.9% .013 255.508);
+    --color-slate-300: oklch(86.9% .022 252.894);
+    --color-slate-400: oklch(70.4% .04 256.788);
+    --color-slate-500: oklch(55.4% .046 257.417);
+    --color-slate-600: oklch(44.6% .043 257.281);
+    --color-slate-700: oklch(37.2% .044 257.287);
+    --color-slate-800: oklch(27.9% .041 260.031);
+    --color-slate-900: oklch(20.8% .042 265.755);
+    --color-gray-200: oklch(92.8% .006 264.531);
+    --color-zinc-100: oklch(96.7% .001 286.375);
+    --color-black: #000;
+    --color-white: #fff;
+    --color-navy-900: #11224d;
     --font-size-10: px2rpx(10);
     --font-size-12: px2rpx(12);
     --font-size-13: px2rpx(13);
@@ -69,6 +198,56 @@
     --font-size-36: px2rpx(36);
     --font-size-40: px2rpx(40);
     --el-component-size: px2rpx(40);
+    --ease-in: cubic-bezier(.4, 0, 1, 1);
+    --ease-out: cubic-bezier(0, 0, .2, 1);
+    --ease-in-out: cubic-bezier(.4, 0, .2, 1);
+    --animate-spin: spin 1s linear infinite;
+    --animate-ping: ping 1s cubic-bezier(0, 0, .2, 1) infinite;
+    --animate-pulse: pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;
+    --blur-sm: 8px;
+    --default-transition-duration: .15s;
+    --default-transition-timing-function: cubic-bezier(.4, 0, .2, 1);
+    --default-font-family: var(--font-sans);
+    --default-mono-font-family: var(--font-mono);
+    --font-inter: Inter, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+    --color-navy-50: #e7e9ef;
+    --color-navy-100: #fff;
+    --color-navy-200: #a3adc2;
+    --color-navy-300: #697a9b;
+    --color-navy-400: #5c6b8a;
+    --color-navy-450: #465675;
+    --color-navy-500: #3a3c43;
+    --color-navy-600: #313e59;
+    --color-navy-700: #242726;
+    --color-navy-750: #222e45;
+    --color-navy-800: #202b40;
+    --color-navy-900: #141a18;
+    --color-slate-150: #e9eef5;
+    --color-primary: #4f46e5;
+    --color-primary-focus: #4338ca;
+    --color-secondary-light: #ff57d8;
+    --color-secondary: #141a18;
+    --color-secondary-focus: #242726;
+    --color-accent-light: #242726;
+    --color-accent: #5f5af6;
+    --color-accent-focus: #4d47f5;
+    --color-info: #0ea5e9;
+    --color-info-focus: #0284c7;
+    --color-success: #1ac23a;
+    --color-success-focus: #17a732;
+    --color-warning: #ff9800;
+    --color-warning-focus: #e68200;
+    --color-error: #e32326;
+    --color-error-focus: #f03000;
+    --text-tiny: .625rem;
+    --text-tiny--line-height: .8125rem;
+    --text-tiny-plus: .6875rem;
+    --text-tiny-plus--line-height: .875rem;
+    --text-xs-plus: .8125rem;
+    --text-xs-plus--line-height: 1.125rem;
+    --text-sm-plus: .9375rem;
+    --text-sm-plus--line-height: 1.375rem;
+    --animate-shimmer: shimmer 2s linear infinite;
 }
 
 
@@ -658,15 +837,176 @@ body {
         padding: 0 !important;
     }
 }
+
 // 公共title样式
-.content-title{
-  width: 100%;
-  height: px2rpx(40);
-  line-height: px2rpx(40);
-  box-sizing: border-box;
-  color: #fff;
-  background-color: #102047;
-  padding-left: px2rpx(15);
-  font-size: px2rpx(20);
-  font-weight: bold;
+.content-title {
+    width: 100%;
+    min-height: px2rpx(40);
+    line-height: px2rpx(40);
+    box-sizing: border-box;
+    color: var(--color-white);
+    background-color: var(--color-navy-900);
+    padding: 0 px2rpx(15);
+    font-size: px2rpx(20);
+    font-weight: bold;
+}
+
+.crm-form {
+    :deep(.uni-row1) {
+        .uni-col {
+            padding: 0 10px !important;
+        }
+
+        .base-info-form>span {
+            display: contents;
+        }
+
+        .uni-forms-item {
+            min-height: px2rpx(79);
+            margin-bottom: px2rpx(10);
+        }
+
+        .uni-select,
+        .uni-combox,
+        .uni-easyinput__content,
+        .uni-date-editor--x {
+            border: none !important;
+            background-color: var(--color-zinc-100) !important;
+
+        }
+
+        .uni-date-x {
+            border: none !important;
+            background-color: rgba(195, 195, 195, 0) !important;
+        }
+
+        .uni-easyinput__content {
+            padding: 0 !important;
+        }
+
+        .checklist-text {
+            color: #666 !important;
+        }
+
+        .checklist-box.is--default.is-checked .checkbox__inner {
+            border-color: var(--color-error) !important;
+            background-color: var(--color-error) !important;
+
+        }
+    }
+}
+
+.bg-secondary {
+    background-color: var(--color-secondary) !important;
+    border: 1px solid #333333;
+    color: var(--color-white);
+}
+
+.underline {
+    text-decoration: underline;
+    text-decoration-color: currentColor;
+    text-underline-offset: px2rpx(2);
+    cursor: pointer;
+}
+
+.info-card {
+    margin-top: px2rpx(4);
+    border-radius: px2rpx(8);
+    background-color: white;
+    padding: px2rpx(16);
+    // box-shadow: 0 0 px2rpx(8) rgba(0, 0, 0, 0.1);
+    box-shadow:
+        0 0 px2rpx(4) rgba(0, 0, 0, 0.06),
+        0 px2rpx(6) px2rpx(12) rgba(0, 0, 0, 0.08);
+    box-sizing: border-box;
+
+}
+
+// 状态标签基础样式
+.status-badge {
+    display: inline-flex;
+    align-items: center;
+    padding: 4rpx 12rpx;
+    border-radius: 4rpx;
+    font-size: var(--font-size-24);
+    font-weight: 500;
+    line-height: 1.5;
+    white-space: nowrap;
+    transition: all var(--default-transition-duration) var(--default-transition-timing-function);
+
+    // 当有取消按钮时调整右边距
+    &.has-cancel {
+        margin-right: 8rpx;
+    }
+}
+
+
+// 待处理
+.status-warning {
+    background-color: var(--status-warning-bg);
+    color: var(--status-warning-text);
+}
+
+// 已完成
+.status-success {
+    background-color: var(--status-success-bg);
+    color: var(--status-success-text);
+}
+
+// 进行中
+.status-processing {
+    background-color: var(--status-processing-bg);
+    color: var(--status-processing-text);
+}
+
+// 已拒绝
+.status-danger {
+    background-color: var(--status-danger-bg);
+    color: var(--status-danger-text);
+}
+
+// 已过期
+.status-expired {
+    background-color: var(--status-expired-bg);
+    color: var(--status-expired-text);
+}
+
+// 已取消
+.status-cancelled {
+    background-color: var(--status-cancelled-bg);
+    color: var(--status-cancelled-text);
+}
+
+// 取消按钮容器
+.cancel-btn-wrapper {
+    display: inline-flex;
+    align-items: center;
+}
+
+// 取消按钮样式
+.cancel-btn {
+    display: inline-flex;
+    align-items: center;
+    padding: 4rpx 12rpx;
+    border-radius: 4rpx;
+    font-size: var(--font-size-24);
+    font-weight: 500;
+    line-height: 1.5;
+    color: var(--color-rose-500);
+    background-color: var(--color-white);
+    border: 1px solid var(--color-rose-500);
+    cursor: pointer;
+    transition: all var(--default-transition-duration) var(--default-transition-timing-function);
+    white-space: nowrap;
+
+    &:hover {
+        color: var(--color-white);
+        background-color: var(--color-rose-500);
+        border-color: var(--color-rose-500);
+    }
+
+    // 添加点击效果
+    &:active {
+        transform: scale(0.95);
+    }
 }

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików