Parcourir la source

feature: 全球速汇

ljc il y a 5 mois
Parent
commit
a1374da796
33 fichiers modifiés avec 1377 ajouts et 9 suppressions
  1. 1 1
      package.json
  2. BIN
      src/assets/image/card/globalOrder/AUD.png
  3. BIN
      src/assets/image/card/globalOrder/BDT.png
  4. BIN
      src/assets/image/card/globalOrder/CAD.png
  5. BIN
      src/assets/image/card/globalOrder/CNY.png
  6. BIN
      src/assets/image/card/globalOrder/EUR.png
  7. BIN
      src/assets/image/card/globalOrder/GBP.png
  8. BIN
      src/assets/image/card/globalOrder/HKD.png
  9. BIN
      src/assets/image/card/globalOrder/IDR.png
  10. BIN
      src/assets/image/card/globalOrder/INR.png
  11. BIN
      src/assets/image/card/globalOrder/JPY.png
  12. BIN
      src/assets/image/card/globalOrder/KRW.png
  13. BIN
      src/assets/image/card/globalOrder/MNT.png
  14. BIN
      src/assets/image/card/globalOrder/MYR.png
  15. BIN
      src/assets/image/card/globalOrder/NZD.png
  16. BIN
      src/assets/image/card/globalOrder/PHP.png
  17. BIN
      src/assets/image/card/globalOrder/SGD.png
  18. BIN
      src/assets/image/card/globalOrder/THB.png
  19. BIN
      src/assets/image/card/globalOrder/TWD.png
  20. BIN
      src/assets/image/card/globalOrder/USD.png
  21. BIN
      src/assets/image/card/globalOrder/VND.png
  22. BIN
      src/assets/image/card/globalOrder/icon-USD.png
  23. 18 0
      src/assets/image/card/globalOrder/icon-arrow.svg
  24. 6 0
      src/assets/image/card/globalOrder/icon-copy.svg
  25. 0 0
      src/enum/card/globalOrder.ts
  26. 4 0
      src/service/financial.ts
  27. 12 0
      src/service/ucard.ts
  28. 14 0
      src/utils/isImageType.ts
  29. 25 0
      src/utils/utils.ts
  30. 261 0
      src/views/card/CardGlobalOrder/components/DynamicForm.vue
  31. 796 0
      src/views/card/CardGlobalOrder/components/GlobalOrderDialog.vue
  32. 232 0
      src/views/card/CardGlobalOrder/components/dialog.scss
  33. 8 8
      src/views/card/CardGlobalOrder/index.vue

+ 1 - 1
package.json

@@ -19,7 +19,7 @@
   "dependencies": {
     "@better-scroll/core": "^2.4.2",
     "@vue/runtime-dom": "^3.5.24",
-    "@vueuse/core": "^9.1.1",
+    "@vueuse/core": "^9.13.0",
     "@wangeditor/editor": "^5.1.14",
     "@wangeditor/editor-for-vue": "^5.1.12",
     "axios": "^0.27.2",

BIN
src/assets/image/card/globalOrder/AUD.png


BIN
src/assets/image/card/globalOrder/BDT.png


BIN
src/assets/image/card/globalOrder/CAD.png


BIN
src/assets/image/card/globalOrder/CNY.png


BIN
src/assets/image/card/globalOrder/EUR.png


BIN
src/assets/image/card/globalOrder/GBP.png


BIN
src/assets/image/card/globalOrder/HKD.png


BIN
src/assets/image/card/globalOrder/IDR.png


BIN
src/assets/image/card/globalOrder/INR.png


BIN
src/assets/image/card/globalOrder/JPY.png


BIN
src/assets/image/card/globalOrder/KRW.png


BIN
src/assets/image/card/globalOrder/MNT.png


BIN
src/assets/image/card/globalOrder/MYR.png


BIN
src/assets/image/card/globalOrder/NZD.png


BIN
src/assets/image/card/globalOrder/PHP.png


BIN
src/assets/image/card/globalOrder/SGD.png


BIN
src/assets/image/card/globalOrder/THB.png


BIN
src/assets/image/card/globalOrder/TWD.png


BIN
src/assets/image/card/globalOrder/USD.png


BIN
src/assets/image/card/globalOrder/VND.png


BIN
src/assets/image/card/globalOrder/icon-USD.png


+ 18 - 0
src/assets/image/card/globalOrder/icon-arrow.svg

@@ -0,0 +1,18 @@
+<svg width="57" height="57" viewBox="0 0 57 57" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g filter="url(#filter0_d_946_656)">
+<circle cx="28.1238" cy="27.1863" r="24.3739" fill="white"/>
+</g>
+<path d="M15.9368 27.6546H39.3732L31.4048 19.6862" stroke="#1FB949" stroke-width="3.74983" stroke-linecap="round"/>
+<defs>
+<filter id="filter0_d_946_656" x="4.74453e-05" y="5.06639e-06" width="56.2475" height="56.2475" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dy="0.937458"/>
+<feGaussianBlur stdDeviation="1.87492"/>
+<feComposite in2="hardAlpha" operator="out"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_946_656"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_946_656" result="shape"/>
+</filter>
+</defs>
+</svg>

+ 6 - 0
src/assets/image/card/globalOrder/icon-copy.svg

@@ -0,0 +1,6 @@
+<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect width="19" height="19" rx="4" fill="#777777"/>
+<rect x="3" y="4" width="9" height="2" rx="1" fill="white"/>
+<rect x="3" y="8" width="13" height="2" rx="1" fill="white"/>
+<rect x="3" y="12" width="13" height="2" rx="1" fill="white"/>
+</svg>

+ 0 - 0
src/enum/card/globalOrder.js → src/enum/card/globalOrder.ts


+ 4 - 0
src/service/financial.ts

@@ -15,6 +15,10 @@ class FinancialService extends Service {
   async withdrawAmountStatistics(params = {}) {
     return await this.post('/finance/withdraw/amount/statictics', params)
   }
+  //根据条件查看拒绝列表-用于下拉和选择展示理由
+  async reasonsRefusalList(params = {}) {
+    return await this.post('/reasons/refusal/list', params)
+  }
 }
 
 export default new FinancialService()

+ 12 - 0
src/service/ucard.ts

@@ -299,6 +299,18 @@ class UCardService extends Service {
   async globalCancelOrder(params = {}) {
     return await this.post('/wasabi/global/cancel/order', params)
   }
+  // 速汇订单审批
+  async globalOrderApprove(params = {}) {
+    return await this.post('/wasabi/global/order/approve', params)
+  }
+  // 补充资料
+  async globalSupplementary(params = {}) {
+    return await this.post('/wasabi/global/supplementary/data', params)
+  }
+  //速汇订单分页列表
+  async globalOrdersList(params = {}) {
+    return await this.post('/wasabi/global/order/page/list', params)
+  }
 }
 
 export default new UCardService()

+ 14 - 0
src/utils/isImageType.ts

@@ -0,0 +1,14 @@
+export default {
+  /**
+   * 检查是否是图片pdf
+   * @param val url
+   */
+  checkFile: function (val) {
+    const imageType = ['image/jpeg', 'image/png', 'image/jpg', 'application/pdf']
+
+    return imageType.indexOf(val.toLocaleLowerCase()) != -1
+  },
+  checkSize: function (val) {
+    return val / 1024 / 1024 < 20
+  },
+}

+ 25 - 0
src/utils/utils.ts

@@ -0,0 +1,25 @@
+/**
+ * 复制文本到剪切板
+ * @param {String} text - 复制的文本
+ */
+export function copyText(text = '') {
+  if (navigator.clipboard) {
+    // clipboard api 复制
+    navigator.clipboard.writeText(text)
+  } else {
+    const textarea = document.createElement('textarea')
+    document.body.appendChild(textarea)
+    // 隐藏此输入框
+    textarea.style.position = 'fixed'
+    textarea.style.clip = 'rect(0 0 0 0)'
+    textarea.style.top = '10px'
+    // 赋值
+    textarea.value = text
+    // 选中
+    textarea.select()
+    // 复制
+    document.execCommand('copy', true)
+    // 移除输入框
+    document.body.removeChild(textarea)
+  }
+}

+ 261 - 0
src/views/card/CardGlobalOrder/components/DynamicForm.vue

@@ -0,0 +1,261 @@
+<template>
+  <div class="dynamic-form">
+    <el-form
+      ref="globalFormRef"
+      :rules="rules"
+      :model="model"
+      label-position="top"
+      class="business-edit-form"
+    >
+      <el-row :gutter="20">
+        <slot></slot>
+        <!-- 遍历每个分组 -->
+        <template v-for="(group, groupKey) in groupedFields" :key="groupKey">
+          <!-- 分组标题 -->
+          <el-col :span="24"
+            ><h3 class="group-title">{{ getGroupTitle(groupKey) }}</h3></el-col
+          >
+          <el-col v-for="field in group" :key="field.id || field.fieldName" :span="12">
+            <el-form-item
+              :label="t('global.fieldName.' + field.fieldName + '.fieldTitle')"
+              :prop="field.fieldName"
+              :rules="getRules(field)"
+              :class="field.hidden ? 'div-hidden' : ''"
+            >
+              <!-- 输入框 -->
+              <el-input
+                v-if="field.fieldType === 'input'"
+                v-model="model[field.fieldName]"
+                :disabled="field.disabled || !!field.fixedValue"
+                :placeholder="t('global.fieldName.' + field.fieldName + '.fieldDescription')"
+                @change="() => clearFieldValidate(field)"
+              />
+              <!-- 下拉框 -->
+              <el-select
+                v-else-if="field.fieldType === 'select'"
+                v-model="model[field.fieldName]"
+                :disabled="field.disabled || !!field.fixedValue"
+                :placeholder="t('global.fieldName.' + field.fieldName + '.fieldDescription')"
+                @change="(val) => clearFieldValidate(field, val, true)"
+              >
+                <el-option
+                  v-for="opt in field.availableDtos || []"
+                  :key="opt.valueId"
+                  :label="t('global.available')[opt.value] || opt.value"
+                  :value="opt.valueId"
+                />
+              </el-select>
+              <!-- 日期 -->
+              <el-date-picker
+                v-else-if="field.fieldType === 'date'"
+                v-model="model[field.fieldName]"
+                :disabled="field.disabled || !!field.fixedValue"
+                type="date"
+                style="width: 100% !important"
+                value-format="YYYY-MM-DD"
+                :start-placeholder="t('Placeholder.Start')"
+                :end-placeholder="t('Placeholder.End')"
+                @change="() => clearFieldValidate(field)"
+              />
+              <!-- 数字 -->
+              <el-input-number
+                v-else-if="field.fieldType === 'number'"
+                v-model="model[field.fieldName]"
+                :min="field.min"
+                :max="field.max"
+                :disabled="field.disabled || !!field.fixedValue"
+                :placeholder="t('global.fieldName.' + field.fieldName + '.fieldDescription')"
+              />
+              <!-- 其他类型 -->
+              <span v-else>未支持类型:{{ field.fieldType }}</span>
+            </el-form-item>
+          </el-col>
+        </template>
+      </el-row>
+    </el-form>
+  </div>
+</template>
+
+<script setup>
+  import { ref, computed, watch, nextTick, toRef } from 'vue'
+  import { useI18n } from 'vue-i18n'
+  import { useVModel } from '@vueuse/core'
+
+  const { t } = useI18n()
+
+  // 定义props
+  const props = defineProps({
+    fields: {
+      type: Array,
+      required: true,
+      default: () => [],
+    },
+    globalForm: {
+      type: Object,
+      required: true,
+    },
+  })
+  // 定义emits
+  const emit = defineEmits(['update:globalForm'])
+
+  // 使用useVModel实现v-model双向绑定
+  const model = useVModel(props, 'globalForm', emit)
+
+  // 响应式数据
+  const globalFormRef = ref(null)
+  const rules = ref({
+    cId: [
+      {
+        required: true,
+        message: t('global.placeholder.p1'),
+        trigger: 'change',
+      },
+    ],
+    deductionAccount: [
+      {
+        required: true,
+        message: t('global.placeholder.p2'),
+        trigger: 'change',
+      },
+    ],
+  })
+
+  // 计算属性
+  const groupedFields = computed(() => {
+    const order = { common: 1, receiver: 3, sender: 2 }
+
+    // 先排序
+    const sorted = [...(props.fields || [])].sort((a, b) => {
+      const typeA = order[a.fieldUserType] || 99
+      const typeB = order[b.fieldUserType] || 99
+      if (typeA !== typeB) return typeA - typeB
+      return (a.sorting || 0) - (b.sorting || 0)
+    })
+
+    // 再分组
+    const groups = {}
+    sorted.forEach((field) => {
+      const type = field.fieldUserType || 'other'
+      if (!groups[type]) groups[type] = []
+      groups[type].push(field)
+    })
+    return groups
+  })
+
+  // 方法
+  const getRules = (field) => {
+    const rules = []
+    if (field.required) {
+      rules.push({
+        required: true,
+        message: `${field.fieldTitle} ${t('Ucard.GlobalOrder.RulesRequire')}`,
+        trigger: 'blur',
+      })
+    }
+    if (field.regex) {
+      try {
+        const reg = new RegExp(field.regex)
+        rules.push({
+          pattern: reg,
+          message: `${field.fieldTitle} ${t('Ucard.GlobalOrder.RulesReg')}`,
+          trigger: 'blur',
+        })
+      } catch (e) {
+        console.warn('Invalid regex:', field.regex)
+      }
+    }
+    return rules
+  }
+
+  const getGroupTitle = (type) => {
+    const map = {
+      common: 'Ucard.GlobalOrder.common',
+      receiver: 'Ucard.GlobalOrder.receiver',
+      sender: 'Ucard.GlobalOrder.sender',
+      other: 'Ucard.GlobalOrder.other',
+    }
+    return t(map[type] || type)
+  }
+
+  const clearFieldValidate = (field, val, isSelect = false) => {
+    nextTick(() => {
+      const fieldName = field.fieldName
+      if (globalFormRef.value) {
+        globalFormRef.value.clearValidate(fieldName)
+      }
+      if (isSelect) {
+        const label = field.availableDtos?.find((item) => item.valueId == val)?.value ?? null
+
+        // 更新表单中的value字段
+        model.value[fieldName + 'Value'] = label
+      }
+    })
+  }
+
+  watch(
+    () => props.globalForm,
+    (newval) => {
+      console.log(newval, 'global ')
+    }
+  )
+  // 监听fields变化
+  watch(
+    () => props.fields,
+    (newFields) => {
+      if (!newFields || !Array.isArray(newFields)) return
+
+      const updatedForm = { ...model.value }
+      let hasChanges = false
+
+      newFields.forEach((field) => {
+        // 调试信息
+        if ('receiverBankCity' === field.fieldName) {
+          console.log('receiverBankCity field:', field)
+        }
+
+        // 有 fixedValue 时回显
+        if (field.fixedValue !== undefined && field.fixedValue !== null) {
+          if (updatedForm[field.fieldName] !== field.fixedValue) {
+            updatedForm[field.fieldName] = field.fixedValue
+            hasChanges = true
+          }
+        } else if (!(field.fieldName in updatedForm)) {
+          // 没有则初始化为空
+          updatedForm[field.fieldName] = null
+          hasChanges = true
+        }
+      })
+
+      // 如果有变化,则更新model
+      if (hasChanges) {
+        model.value = updatedForm
+      }
+
+      nextTick(() => {
+        if (globalFormRef.value) {
+          globalFormRef.value.clearValidate()
+        }
+      })
+    },
+    {
+      immediate: true,
+      deep: true,
+    }
+  )
+</script>
+
+<style scoped lang="scss">
+  .group-title {
+    font-weight: bold;
+    font-size: 16px;
+    color: #333;
+    margin: 20px 0 10px;
+    border-left: 4px solid #409eff;
+    padding-left: 8px;
+    text-align: left;
+  }
+
+  .form-group + .form-group {
+    margin-top: 30px;
+  }
+</style>

+ 796 - 0
src/views/card/CardGlobalOrder/components/GlobalOrderDialog.vue

@@ -0,0 +1,796 @@
+<template>
+  <el-dialog
+    v-model="dialogVisible"
+    :title="diaTitle"
+    width="1343px"
+    class="info-dialog"
+    @close="closeDia"
+  >
+    <div class="dialog-content">
+      <div class="title">
+        {{ t('Ucard.GlobalOrder.info1') }}
+      </div>
+      <!-- 分割线 -->
+      <el-divider></el-divider>
+      <div class="payout">
+        <div class="payout_pay">
+          <div class="money">
+            {{ detailData.deductionAmount?.toFixed(2) || '- -' }}
+          </div>
+          <div class="pay-bot">
+            <img :src="currencyIcon(detailData.sendCurrency)" alt="" />
+            <span>
+              {{ detailData.sendCurrency }}
+            </span>
+          </div>
+        </div>
+        <div class="arrow">
+          <img src="@/assets/image/card/globalOrder/icon-arrow.svg" alt="" />
+        </div>
+        <div class="payout_pay">
+          <div class="money">
+            {{ detailData.transferAmount?.toFixed(2) || '- -' }}
+          </div>
+          <div class="pay-bot">
+            <img :src="currencyIcon(detailData.payoutCurrency)" alt="" />
+            <span>
+              {{ detailData.payoutCurrency }}
+            </span>
+          </div>
+        </div>
+      </div>
+      <div class="payout_info">
+        <el-row>
+          <el-col v-for="(info, index) in payoutInfo" :key="index" :span="6">
+            <div class="info-item">
+              <div class="item-tit">
+                {{ t(`Ucard.GlobalOrder.infoT${index + 1}`) }}
+              </div>
+              <div class="item-desc">
+                {{ getInfoValue(index) }}
+                <span
+                  v-if="index === 0"
+                  class="item-other"
+                  @click="copyText(detailData.merchantOrderNo)"
+                >
+                  <img src="@/assets/image/card/globalOrder/icon-copy.svg" alt="" />
+                </span>
+                <span v-else-if="index === 1" class="item-other">
+                  {{ detailData.payoutCurrency }}
+                </span>
+                <span v-else-if="index === 2" class="item-other">
+                  {{ detailData.fee ? detailData.payoutCurrency : '' }}
+                </span>
+              </div>
+            </div>
+          </el-col>
+        </el-row>
+      </div>
+      <el-tabs v-model="activeName" class="tabs" @tab-click="tabsClick">
+        <el-tab-pane :label="t('Ucard.GlobalOrder.sender')" name="1">
+          <div class="tab-item">
+            <el-row justify="space-between">
+              <el-col v-for="(item, index) in list.sender" :key="index" :span="6">
+                <div class="item-tit">
+                  {{ item.name }}
+                </div>
+                <div class="item-desc">
+                  {{ item.value || '- -' }}
+                </div>
+              </el-col>
+            </el-row>
+          </div>
+        </el-tab-pane>
+        <el-tab-pane :label="t('Ucard.GlobalOrder.receiver')" name="2">
+          <div class="tab-item">
+            <el-row justify="">
+              <el-col v-for="(item, index) in list.receiver" :key="index" :span="6">
+                <div class="item-tit">
+                  {{ item.name }}
+                </div>
+                <div class="item-desc">
+                  {{ item.value || '- -' }}
+                </div>
+              </el-col>
+            </el-row>
+          </div>
+        </el-tab-pane>
+      </el-tabs>
+
+      <el-form
+        v-if="type === '2'"
+        ref="formRef"
+        :model="approveForm"
+        :rules="rules"
+        class="dialogCheck_form"
+        label-width="135px"
+      >
+        <el-form-item :label="t('card.Form.f53') + ':'" prop="status">
+          <el-select
+            v-model="approveForm.status"
+            :placeholder="t('Placeholder.Choose')"
+            class="crm_search_down form-w300"
+          >
+            <el-option
+              v-for="option in statusOptions"
+              :key="option.value"
+              :label="option.label"
+              :value="option.value"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          v-if="approveForm.status == 3"
+          :label="t('Label.Descr') + ':'"
+          prop="approveDesc"
+        >
+          <el-select
+            v-model="approveForm.approveDesc"
+            :placeholder="t('Placeholder.Choose')"
+            allow-create
+            class="crm_search_down form-w300"
+            filterable
+            @change="selectChange"
+          >
+            <el-option
+              v-for="item in reasonsList"
+              :key="item.id"
+              :label="langZh ? item.content : item.enContent"
+              :value="item.id"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+
+      <el-form
+        v-if="complianceStatus"
+        ref="complianceFormRef"
+        :model="complianceForm"
+        label-width="135px"
+      >
+        <div v-if="complianceItems.senderItems.length" class="title">
+          {{ t('Ucard.GlobalOrder.SenderComSubmit') }}
+          <el-divider></el-divider>
+        </div>
+        <div v-for="field in complianceItems.senderItems" :key="field.fieldName">
+          <el-form-item :label="field.fieldTitle" :prop="field.fieldName" :rules="getRules(field)">
+            <el-input
+              v-if="field.fieldType === 'input'"
+              v-model="complianceForm[field.fieldName]"
+              :disabled="field.disabled"
+              class="form-w300"
+              :placeholder="field.fieldDescription"
+              @change="() => clearFieldValidate(field.fieldName)"
+            />
+            <!-- 下拉框 -->
+            <el-select
+              v-else-if="field.fieldType === 'select'"
+              v-model="complianceForm[field.fieldName]"
+              :disabled="field.disabled"
+              class="form-w300"
+              :placeholder="field.fieldDescription"
+              @change="clearFieldValidate(field.fieldName)"
+            >
+              <el-option
+                v-for="opt in field.availableDtos || []"
+                :key="opt.valueId"
+                :label="t('global.available')[opt.value] || opt.value"
+                :value="opt.valueId"
+              />
+            </el-select>
+            <!-- 日期 -->
+            <el-date-picker
+              v-else-if="field.fieldType === 'date'"
+              v-model="complianceForm[field.fieldName]"
+              :disabled="field.disabled"
+              type="date"
+              class="form-w300"
+              value-format="YYYY-MM-DD"
+              :start-placeholder="t('Placeholder.Start')"
+              :end-placeholder="t('Placeholder.End')"
+              @change="clearFieldValidate(field.fieldName)"
+            />
+            <!-- 文件 -->
+            <div v-if="field.fieldType === 'file'">
+              <div v-if="!field.disabled">
+                <el-upload
+                  class="avatar-uploader"
+                  :http-request="(e) => comUpload(e, field)"
+                  :disabled="field.disabled"
+                  :show-file-list="false"
+                  :before-upload="beforeUpload"
+                >
+                  <img
+                    v-if="
+                      complianceForm[field.fieldName] && !isPdf(complianceForm[field.fieldName])
+                    "
+                    :src="complianceForm[field.fieldName]"
+                    class="avatar"
+                    alt=""
+                  />
+                  <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
+                </el-upload>
+                <a
+                  v-if="complianceForm[field.fieldName] && isPdf(complianceForm[field.fieldName])"
+                  class="upload-pdf"
+                  target="_blank"
+                  :href="complianceForm[field.fieldName]"
+                >
+                  {{ getFieldName(complianceForm[field.fieldName]) }}
+                  <el-icon><Document /></el-icon>
+                </a>
+              </div>
+              <div v-else>
+                <a
+                  v-if="complianceForm[field.fieldName] && isPdf(complianceForm[field.fieldName])"
+                  class="upload-pdf"
+                  target="_blank"
+                  :href="complianceForm[field.fieldName]"
+                >
+                  {{ getFieldName(complianceForm[field.fieldName]) }}
+                  <el-icon><Document /></el-icon>
+                </a>
+                <el-image
+                  v-else
+                  style="width: 200px; height: 200px"
+                  :lazy="false"
+                  :src="complianceForm[field.fieldName]"
+                  :preview-src-list="[complianceForm[field.fieldName]]"
+                >
+                  <template #error>
+                    <div class="image-slot">
+                      <el-icon><Picture /></el-icon>
+                    </div>
+                  </template>
+                </el-image>
+              </div>
+            </div>
+          </el-form-item>
+        </div>
+        <div v-if="complianceItems.receiverItems.length" style="margin-bottom: 20px">
+          {{ t('Ucard.GlobalOrder.ReceiverComSubmit') }}
+        </div>
+        <div v-for="field in complianceItems.receiverItems" :key="field.fieldName">
+          <el-form-item :label="field.fieldTitle" :prop="field.fieldName" :rules="getRules(field)">
+            <el-input
+              v-if="field.fieldType === 'input'"
+              v-model="complianceForm[field.fieldName]"
+              :disabled="field.disabled"
+              :placeholder="field.fieldDescription"
+              @change="clearFieldValidate(field.fieldName)"
+            />
+            <!-- 下拉框 -->
+            <el-select
+              v-else-if="field.fieldType === 'select'"
+              v-model="complianceForm[field.fieldName]"
+              :disabled="field.disabled"
+              :placeholder="field.fieldDescription"
+              @change="clearFieldValidate(field.fieldName)"
+            >
+              <el-option
+                v-for="opt in field.availableDtos || []"
+                :key="opt.valueId"
+                :label="t('global.available')[opt.value] || opt.value"
+                :value="opt.valueId"
+              />
+            </el-select>
+            <!-- 日期 -->
+            <el-date-picker
+              v-else-if="field.fieldType === 'date'"
+              v-model="complianceForm[field.fieldName]"
+              :disabled="field.disabled"
+              type="date"
+              value-format="YYYY-MM-DD"
+              :start-placeholder="t('Placeholder.Start')"
+              :end-placeholder="t('Placeholder.End')"
+              @change="clearFieldValidate(field.fieldName)"
+            />
+            <!-- 文件 -->
+            <div v-if="field.fieldType === 'file'">
+              <div v-if="!field.disabled">
+                <el-upload
+                  class="avatar-uploader"
+                  :http-request="(e) => comUpload(e, field)"
+                  :disabled="field.disabled"
+                  :show-file-list="false"
+                  :before-upload="beforeUpload"
+                >
+                  <img
+                    v-if="
+                      complianceForm[field.fieldName] && !isPdf(complianceForm[field.fieldName])
+                    "
+                    :src="complianceForm[field.fieldName]"
+                    class="avatar"
+                    alt=""
+                  />
+                  <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
+                </el-upload>
+                <a
+                  v-if="complianceForm[field.fieldName] && isPdf(complianceForm[field.fieldName])"
+                  class="upload-pdf"
+                  target="_blank"
+                  :href="complianceForm[field.fieldName]"
+                >
+                  {{ getFieldName(complianceForm[field.fieldName]) }}
+                  <el-icon><Document /></el-icon>
+                </a>
+              </div>
+              <div v-else>
+                <a
+                  v-if="complianceForm[field.fieldName] && isPdf(complianceForm[field.fieldName])"
+                  class="upload-pdf"
+                  target="_blank"
+                  :href="complianceForm[field.fieldName]"
+                >
+                  {{ getFieldName(complianceForm[field.fieldName]) }}
+                  <el-icon><Document /></el-icon>
+                </a>
+                <el-image
+                  v-else
+                  style="width: 200px; height: 200px"
+                  :lazy="false"
+                  :src="complianceForm[field.fieldName]"
+                  :preview-src-list="[complianceForm[field.fieldName]]"
+                >
+                  <template #error>
+                    <div class="image-slot">
+                      <el-icon><Picture /></el-icon>
+                    </div>
+                  </template>
+                </el-image>
+              </div>
+            </div>
+          </el-form-item>
+        </div>
+      </el-form>
+    </div>
+    <template #footer>
+      <el-button v-if="type === '2'" type="primary" @click="toConfirm()">
+        {{ t('card.Btn.Submit') }}
+      </el-button>
+      <el-button v-if="type === '3'" type="primary" @click="toComConfirm()">
+        {{ t('card.Btn.Submit') }}
+      </el-button>
+      <el-button @click="closeDia">
+        {{ t('Btn.Cancel') }}
+      </el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+  import { ref, reactive, computed, watch, onMounted, nextTick, inject } from 'vue'
+  import { useI18n } from 'vue-i18n'
+  import { ElMessage, ElMessageBox } from 'element-plus'
+  import {
+    complianceStatusEnum,
+    payoutMethod,
+    transferType,
+    approvalText,
+  } from '@/enum/card/globalOrder'
+  import Service from '@/service/financial'
+  import CardService from '@/service/ucard'
+  import Config from '@/config'
+  import isImageType from '@/utils/isImageType'
+  import UcardService from '@/service/ucard'
+  import { safeGetUser } from '@/utils/safeJson'
+  import { copyText } from '@/utils/utils'
+  import { Document, Picture, Plus } from '@element-plus/icons-vue'
+  import _ from 'lodash'
+
+  const { t } = useI18n()
+  const { Code, Host85 } = Config
+  const Session = inject('session')
+
+  // 定义props
+  const props = defineProps({
+    // 1 详情窗口,2 审批窗口, 3 提交合规性检查资料
+    type: {
+      type: String,
+      required: true,
+      default: '1',
+    },
+    visible: {
+      type: Boolean,
+      required: true,
+    },
+    detailData: {
+      type: Object,
+      required: true,
+    },
+  })
+
+  // 定义emits
+  const emit = defineEmits(['close', 'update'])
+
+  // 响应式数据
+  const dialogVisible = ref(props.visible)
+  const formRef = ref(null)
+  const complianceFormRef = ref(null)
+  const activeName = ref('1')
+  const imageUrl = ref('')
+  const reasons = ref({})
+
+  const approveForm = reactive({
+    status: '',
+    approveDesc: '',
+  })
+
+  const complianceForm = reactive({})
+
+  const list = reactive({
+    sender: [],
+    receiver: [],
+  })
+
+  const rules = reactive({
+    status: [
+      {
+        required: true,
+        message: t('vaildate.select.empty'),
+        trigger: 'change',
+      },
+    ],
+    approveDesc: [
+      {
+        required: true,
+        message: t('vaildate.select.empty'),
+        trigger: 'change',
+      },
+    ],
+  })
+
+  // 监听visible变化
+  watch(
+    () => props.visible,
+    (val) => {
+      dialogVisible.value = val
+    }
+  )
+
+  // 监听dialogVisible变化
+  watch(dialogVisible, (val) => {
+    if (!val) {
+      emit('close')
+    }
+  })
+
+  // 计算属性
+  const user = computed(() => {
+    return safeGetUser(Session)
+  })
+
+  const langZh = computed(() => {
+    return Session.Get('lang') == 'cn'
+  })
+
+  const approverDesc = computed(() => {
+    if (!reasons.value) return ''
+    const data = reasons.value[props.detailData.approveDesc]
+    return langZh.value ? data?.content : data?.enContent
+  })
+
+  const diaTitle = computed(() => {
+    const titles = {
+      1: t('R-Business-Single'),
+      2: t('R-Business-Approve'),
+      3: t('R-GlobalOrder-Btn'),
+    }
+    return titles[props.type] || ''
+  })
+
+  const complianceStatus = computed(() => {
+    if (props.type === '1') {
+      return props.detailData.complianceStatus !== 'pending_check'
+    }
+    return props.type === '3'
+  })
+
+  const complianceItems = computed(() => {
+    let senderItems = []
+    let receiverItems = []
+
+    if (props.detailData?.dataDtos) {
+      const processItems = (customerType) => {
+        return props.detailData.dataDtos
+          .filter((item) => item.customerType === customerType)
+          .map((item) => {
+            const fieldName = `${item.customerType}_${item.fieldName}`
+            const fieldTitle = t(`global.fieldName.${fieldName}.fieldTitle`) + ':'
+            const fieldDescription = t(`global.fieldName.${fieldName}.fieldDescription`)
+            const disabled = [
+              complianceStatusEnum.Approved,
+              complianceStatusEnum.Pending,
+              complianceStatusEnum.Rejected,
+            ].includes(item.status)
+
+            // 如果处于只读状态,设置表单值
+            if (disabled) {
+              complianceForm[fieldName] =
+                item.fieldType === 'file' ? Host85 + item.rfiValueUrl : item.rfiValue
+            }
+
+            return {
+              ...item,
+              fieldName,
+              fieldTitle,
+              fieldDescription,
+              disabled,
+            }
+          })
+      }
+
+      senderItems = processItems('sender')
+      receiverItems = processItems('receiver')
+    }
+    return {
+      senderItems: senderItems || [],
+      receiverItems: receiverItems || [],
+    }
+  })
+
+  const payoutInfo = computed(() => {
+    return [
+      { label: t('Ucard.GlobalOrder.infoT1') },
+      { label: t('Ucard.GlobalOrder.infoT2') },
+      { label: t('Ucard.GlobalOrder.infoT3') },
+      { label: t('Ucard.GlobalOrder.infoT4') },
+    ]
+  })
+
+  const reasonsList = computed(() => {
+    return Object.values(reasons.value)
+  })
+
+  const statusOptions = computed(() => [
+    { label: t('Apply_info.VerifiedUser.Refused'), value: 3 },
+    { label: t('Apply_info.VerifiedUser.Agree'), value: 2 },
+  ])
+
+  // 方法
+  const currencyIcon = (currency) => {
+    try {
+      return new URL(`../../../../assets/image/card/globalOrder/${currency}.png`, import.meta.url)
+        .href
+    } catch (e) {
+      return ''
+    }
+  }
+
+  const isPdf = (url) => {
+    return /\.pdf(\?.*)?$/i.test(url)
+  }
+
+  const tabsClick = (e) => {
+    activeName.value = e.name
+  }
+
+  const getFieldName = (url) => {
+    try {
+      const urlObj = new URL(url)
+      const pathname = urlObj.pathname
+      const filename = pathname.split('/').pop()
+      return filename || ''
+    } catch (error) {
+      console.error('无效的URL:', error)
+      return ''
+    }
+  }
+
+  const getInfoValue = (index) => {
+    switch (index) {
+      case 0:
+        return props.detailData.merchantOrderNo
+      case 1:
+        return props.detailData.transferAmount
+      case 2:
+        return props.detailData.fee || '- - '
+      case 3:
+        return props.detailData.exchangeRate ? `1 : ${props.detailData.exchangeRate}` : '- - '
+      default:
+        return ''
+    }
+  }
+
+  const closeDia = (update) => {
+    Object.keys(approveForm).forEach((key) => (approveForm[key] = ''))
+    Object.keys(complianceForm).forEach((key) => delete complianceForm[key])
+    if (update === true) {
+      emit('update')
+    }
+    dialogVisible.value = false
+  }
+
+  // 审批
+  const toConfirm = async () => {
+    if (!formRef.value) return
+
+    const valid = await formRef.value.validate()
+    if (!valid) return
+
+    const res = await CardService.globalOrderApprove({
+      ...props.detailData,
+      ...approveForm,
+      operateUser: user.value.cId,
+    })
+
+    if (res.code == Code.StatusOK) {
+      ElMessage.success(res.msg)
+      closeDia(true)
+    } else {
+      ElMessage.error(res.msg)
+    }
+  }
+
+  // 提交合规性文件
+  const toComConfirm = async () => {
+    if (!complianceFormRef.value) return
+
+    const valid = await complianceFormRef.value.validate()
+    if (!valid) return
+
+    const list = [
+      ...(complianceItems.value.senderItems ?? []),
+      ...(complianceItems.value.receiverItems ?? []),
+    ]
+
+    const rfiInfos = list.map((item) => {
+      const { fieldType, rfiId } = item
+      let rfiValue = ''
+      let rfiValueUrl = ''
+
+      if (fieldType === 'file') {
+        rfiValueUrl = complianceForm[item.fieldName]?.split(Host85)[1] || ''
+      } else {
+        rfiValue = complianceForm[item.fieldName] || ''
+      }
+
+      return {
+        rfiId,
+        fieldType,
+        rfiValueUrl,
+        rfiValue,
+      }
+    })
+
+    const params = {
+      cId: user.value.cId,
+      orderNo: props.detailData.orderNo,
+      rfiInfos,
+    }
+
+    const res = await CardService.globalSupplementary(params)
+
+    if (res.code == Code.StatusOK) {
+      ElMessage.success(res.msg)
+      closeDia(true)
+    } else {
+      ElMessage.error(res.msg)
+    }
+  }
+
+  const selectChange = () => {}
+
+  // 获取原因列表
+  const searchReasons = async () => {
+    const res = await Service.reasonsRefusalList({ type: 10 })
+    if (res.code == Code.StatusOK) {
+      reasons.value = res.data || {}
+    } else {
+      ElMessage.error(res.msg)
+    }
+  }
+
+  const clearFieldValidate = (fieldName) => {
+    nextTick(() => {
+      if (complianceFormRef.value) {
+        complianceFormRef.value.clearValidate(fieldName)
+      }
+    })
+  }
+
+  // 表单校验
+  const getRules = (field) => {
+    const rules = []
+    if (field.required) {
+      rules.push({
+        required: true,
+        message: field.fieldDescription,
+        trigger: 'blur',
+      })
+    }
+    if (field.regex) {
+      try {
+        const reg = new RegExp(field.regex)
+        rules.push({
+          pattern: reg,
+          message: `${field.fieldTitle}格式不正确`,
+          trigger: 'blur',
+        })
+      } catch (e) {
+        console.warn('Invalid regex:', field.regex)
+      }
+    }
+    return rules
+  }
+
+  // 合规性检测文件上传
+  const comUpload = async (field, item) => {
+    const formData = new FormData()
+    formData.append('category', 'globalTransfer')
+    formData.append('file', field.file)
+
+    try {
+      const res = await UcardService.ucardUpload(formData)
+      if (res.code === 200 && res.data) {
+        complianceForm[item.fieldName] = Host85 + res.data
+        ElMessage.success(t('Ucard.Business.ms11'))
+      } else {
+        ElMessage.error(res.msg)
+      }
+    } catch (e) {
+      ElMessage.error(t('Ucard.Business.ms12'))
+    }
+  }
+
+  const beforeUpload = (file) => {
+    const isJPG = isImageType.checkFile(file.type)
+    const isLt2M = isImageType.checkSize(file.size)
+
+    if (!isJPG) {
+      ElMessage.error(t('Msg.JPG1'))
+    }
+    if (!isLt2M) {
+      ElMessage.error(t('Msg.3IMG'))
+    }
+    return isJPG && isLt2M
+  }
+
+  // 生命周期
+  onMounted(() => {
+    // 审批才需要原因列表 详情也需要展示
+    if (props.type == '2' || props.detailData.approverStatus != '1') {
+      searchReasons()
+    }
+    const fieldDtos = _.cloneDeep(props.detailData.fieldDtos)
+    console.log(fieldDtos, '12')
+    const sender =
+      fieldDtos
+        ?.sort((a, b) => a.sorting - b.sorting)
+        .filter((item) => item.fieldUserType === 'sender')
+        .map((item) => {
+          const key = Object.keys(props.detailData).find(
+            (k) => k.toLowerCase() === item.fieldName.toLowerCase()
+          )
+          const name = t(`global.fieldName.${item.fieldName}.fieldTitle`)
+          let value = key ? props.detailData[key] : item.fixedValue
+          if (item.fieldType === 'select' && !['transferType', 'payoutMethod'].includes(key)) {
+            value = props.detailData[key + 'Value']
+          }
+          return { name, value }
+        }) || []
+
+    const receiver =
+      fieldDtos
+        ?.sort((a, b) => a.sorting - b.sorting)
+        .filter((item) => item.fieldUserType === 'receiver')
+        .map((item) => {
+          const key = Object.keys(props.detailData).find(
+            (k) => k.toLowerCase() === item.fieldName.toLowerCase()
+          )
+          const name = t(`global.fieldName.${item.fieldName}.fieldTitle`)
+          let value = key ? props.detailData[key] : item.fixedValue
+          if (item.fieldType === 'select' && !['transferType', 'payoutMethod'].includes(key)) {
+            value = props.detailData[key + 'Value']
+          }
+          return { name, value }
+        }) || []
+
+    list.sender = sender
+    list.receiver = receiver
+  })
+</script>
+
+<style lang="scss" scoped>
+  @import 'dialog.scss';
+</style>

+ 232 - 0
src/views/card/CardGlobalOrder/components/dialog.scss

@@ -0,0 +1,232 @@
+
+.info-dialog {
+  .dialog-content {
+    padding: 0 12px;
+
+    .el-divider--horizontal {
+      margin: 15px 0;
+    }
+  }
+
+  .title {
+    text-align: left;
+    color: #333;
+    font-size: 18px;
+    font-style: normal;
+    font-weight: 500;
+    line-height: normal;
+  }
+
+  .payout {
+    margin-top: 15px;
+    text-align: left;
+    display: flex;
+    justify-content: left;
+
+    .payout_pay {
+      margin-right: 55.5px;
+
+      .money {
+        color: #000;
+        font-size: 28px;
+        font-style: normal;
+        font-weight: 700;
+        line-height: normal;
+        margin-bottom: 17px;
+      }
+
+      .pay-bot {
+        display: flex;
+        align-items: flex-start;
+
+        img {
+          width: 25px;
+          height: 25px;
+          border-radius: 50%;
+          margin-right: 9px;
+        }
+
+        color: #333;
+        font-size: 17px;
+        font-style: normal;
+        font-weight: 500;
+        line-height: normal;
+      }
+    }
+
+    .arrow {
+      margin-right: 50px;
+    }
+  }
+
+  .payout_info {
+    //display: flex;
+    //justify-content: space-between;
+    align-items: center;
+    margin-top: 25px;
+    width: 100%;
+    box-sizing: border-box;
+    padding: 22px 24px;
+    border-radius: 8px;
+    background: #F6F5F8;
+
+    .info-item {
+      display: flex;
+      flex-direction: column;
+      align-items: flex-start;
+
+      .item-tit {
+        color: #646373;
+        font-size: 14px;
+        font-style: normal;
+        font-weight: 500;
+      }
+
+      .item-desc {
+        margin-top: 10px;
+        color: #333;
+        font-size: 18px;
+        font-weight: 500;
+        line-height: 21px;
+
+        .item-other {
+          color: #B8B7C8;
+          font-size: 14px;
+          font-weight: 500;
+
+          img {
+            cursor: pointer;
+            width: 19px;
+            height: 19px;
+          }
+        }
+      }
+    }
+  }
+}
+
+.el-col {
+  margin-bottom: 24px;
+}
+
+.form-w300 {
+  width: 300px;
+}
+
+.tabs {
+  margin: 40px 0;
+
+  .tab-item {
+    margin-top: 10px;
+    border-radius: 8px;
+    background: #F8F8F8;
+    width: 100%;
+    box-sizing: border-box;
+    padding: 20px 24px;
+    text-align: left;
+
+    .item-tit {
+      color: #646373;
+      font-size: 14px;
+      font-weight: 500;
+      margin-bottom: 10px;
+    }
+
+    .item-desc {
+      color: #333;
+      font-size: 18px;
+      font-weight: 500;
+    }
+  }
+}
+
+.detail-content {
+  padding: 24px 24px 0 24px;
+
+  .detail-item {
+    display: flex;
+    align-items: flex-start;
+    padding: 12px 0;
+
+    &:last-child {
+      border-bottom: none;
+    }
+
+    .label {
+      width: 110px;
+      min-width: 90px;
+      color: #888;
+      font-size: 14px;
+      font-weight: 500;
+      text-align: right;
+      padding-right: 12px;
+      line-height: 1.6;
+      flex-shrink: 0;
+    }
+
+    .value {
+      flex: 1;
+      color: #222;
+      font-size: 15px;
+      word-break: break-all;
+      text-align: left;
+      line-height: 1.6;
+      margin-left: 10px;
+
+      img {
+        width: 150px;
+        height: auto;
+      }
+    }
+  }
+}
+
+.dialog-footer {
+  padding: 20px 24px;
+  text-align: center;
+
+  .el-button {
+    width: 120px;
+    font-size: 15px;
+    border-radius: 6px;
+    font-weight: 500;
+    letter-spacing: 2px;
+  }
+}
+
+::v-deep .avatar-uploader .el-upload {
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+}
+
+.avatar-uploader .el-upload:hover {
+  border-color: #409EFF;
+}
+
+.avatar-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 178px;
+  height: 178px;
+  line-height: 178px;
+  text-align: center;
+}
+
+.avatar {
+  width: 178px;
+  height: 178px;
+  display: block;
+}
+
+.upload-pdf {
+  //width: 200px;
+  height: 20px;
+  display: block;
+}
+
+::v-deep(.el-form-item__content) {
+  text-align: left;
+}

+ 8 - 8
src/views/card/CardGlobalOrder/index.vue

@@ -508,7 +508,7 @@
   import _ from 'lodash'
   import Config from '@/config/index'
   import UcardService from '@/service/ucard'
-  import { exportExcel } from '@/utils/export'
+  import { exportExcel } from '@/utils/export.js'
   import DynamicForm from './components/DynamicForm.vue'
   import GlobalOrderDialog from './components/GlobalOrderDialog.vue'
   import {
@@ -1083,11 +1083,11 @@
         break
       case '4':
         ElMessageBox.confirm(
-          $t('Ucard.GlobalOrder.ConfirmCancelOrder'),
-          $t('Ucard.GlobalOrder.CancelOrder'),
+          t('Ucard.GlobalOrder.ConfirmCancelOrder'),
+          t('Ucard.GlobalOrder.CancelOrder'),
           {
-            confirmButtonText: $t('Btn.Confirm'),
-            cancelButtonText: $t('Btn.Cancel'),
+            confirmButtonText: t('Btn.Confirm'),
+            cancelButtonText: t('Btn.Cancel'),
             type: 'warning',
           }
         )
@@ -1144,13 +1144,13 @@
         businessList.value = res.data
         pagerInfo.rowTotal = res.page?.rowTotal || 0
         pagerInfo.pageTotal = res.page?.pageTotal || 0
-        ElMessage.success($t('Msg.SearchSuccess'))
+        ElMessage.success(t('Msg.SearchSuccess'))
       } else {
-        ElMessage.error(res.msg || $t('Ucard.Business.ms2'))
+        ElMessage.error(res.msg || t('Ucard.Business.ms2'))
       }
     } catch (error) {
       console.error('Search error:', error)
-      ElMessage.error($t('Ucard.Business.ms2'))
+      ElMessage.error(t('Ucard.Business.ms2'))
     } finally {
       pictLoading.value = false
     }