zhb 3 tygodni temu
rodzic
commit
930e79e170

+ 295 - 13
components/cwg-file-picker-wrapper.vue

@@ -4,7 +4,8 @@
     <view v-if="readonly" class="file-list readonly-list">
       <view v-for="(file, index) in innerFileList" :key="index" class="file-item readonly-item"
         @click="previewFile(file, index)">
-        <image v-if="isImage(file)" :src="file.url || file.path" mode="aspectFill" class="file-thumb" :style="imgStyle" />
+        <image v-if="isImage(file)" :src="file.url || file.path" mode="aspectFill" class="file-thumb"
+          :style="imgStyle" />
         <view v-else class="file-icon" :class="getFileClass(file.name)">
           <text class="file-icon-text">{{ getFileIcon(file.name) }}</text>
         </view>
@@ -15,11 +16,12 @@
 
     <!-- 正常模式:宫格上传(完全对齐官方 upload-image 样式) -->
     <view v-else class="uni-file-picker__container">
-      <view class="file-picker__box" v-for="(item, index) in innerFileList" :key="index" :style="typeof boxStyle === 'object' ? boxStyle : { cssText: boxStyle }">
+      <view class="file-picker__box" v-for="(item, index) in innerFileList" :key="index"
+        :style="typeof boxStyle === 'object' ? boxStyle : { cssText: boxStyle }">
         <view class="file-picker__box-content" :style="borderStyle">
           <!-- 图片 -->
-          <image v-if="isImage(item)" class="file-image" :src="item.url || item.path" mode="aspectFill" :style="imgStyle"
-            @click.stop="previewFile(item, index)" />
+          <image v-if="isImage(item)" class="file-image" :src="item.url || item.path" mode="aspectFill"
+            :style="imgStyle" @click.stop="previewFile(item, index)" />
 
           <!-- 视频 → 显示第一帧 + 播放图标 -->
           <view v-else-if="isVideo(item)" class="file-cover video-box" @click.stop="previewFile(item, index)">
@@ -54,20 +56,23 @@
       </view>
 
       <!-- 添加按钮 -->
-      <view v-if="innerFileList?.length < limit" class="file-picker__box" :style="typeof boxStyle === 'object' ? boxStyle : { cssText: boxStyle }">
+      <view v-if="innerFileList?.length < limit" class="file-picker__box"
+        :style="typeof boxStyle === 'object' ? boxStyle : { cssText: boxStyle }">
         <view class="file-picker__box-content is-add" :style="borderStyle" @click="handleChoose">
           <cwg-icon name="icon_add" class="upload-icon" :size="24" />
         </view>
       </view>
     </view>
   </view>
+  <cop-chooseFile :trigger="triggerFlag" accept="*" @receiveRenderFile="handleFile">
+  </cop-chooseFile>
 </template>
 
 <script setup>
-import { ref, watch, nextTick,computed } from 'vue'
+import { ref, watch, nextTick, computed } from 'vue'
 import config from '@/config'
 import { userToken } from '@/composables/config'
-
+import copChooseFile from '@/uni_modules/cop-chooseFile/components/cop-chooseFile/cop-chooseFile.vue'
 // === Vue3 v-model 标准写法 + 多类型兼容 ===
 const props = defineProps({
   modelValue: {
@@ -156,6 +161,7 @@ const emit = defineEmits([
   'progress',
   'select'
 ])
+const triggerFlag = ref(0)
 
 // 内部数据
 const innerFileList = ref([])
@@ -270,7 +276,6 @@ watch(
 
       // 只返回干净的 path 数据
       const cleanList = list.map(item => item.path || item.url || '')
-      console.log(originalType);
 
       // 按原始类型返回
       if (props.limit === 1) {
@@ -361,6 +366,7 @@ const deleteFile = (index) => {
 const handleChoose = () => {
   if (props.disabled || props.readonly) return
   const count = props.limit - innerFileList.value.length
+  // #ifdef H5
   uni.chooseFile({
     type: props.fileMediatype,
     count,
@@ -373,12 +379,105 @@ const handleChoose = () => {
       if (props.autoUpload) startUpload()
     }
   })
-}
+  // #endif
+  // #ifdef APP-PLUS
+  triggerFlag.value = Date.now()
+  // chooseFileFromModule({
+  //   complete: (res) => {
+  //     console.log(res)
+  //     let path = res.path
+  //     let name = res.name
+  //     let fileType = ''
+  //     // 如果没有name,默认为:截取最后一个/之后的内容
+  //     if (!name) {
+  //       let lastIndex = path.lastIndexOf('/');
+  //       if (lastIndex !== -1) {
+  //         name = path.substring(lastIndex + 1);
+  //       } else {
+  //         name = Math.random().toString(36).substr(2) + Date.now();
+  //       }
+  //     }
+  //     // 使用 lastIndexOf 方法找到最后一个 . 的位置
+  //     let lastDotIndex = name.lastIndexOf('.');
+  //     if (lastDotIndex !== -1) {
+  //       fileType = name.substring(lastDotIndex + 1);
+  //     } else {
+  //       console.log('文件路径中没有 .');
+  //     }
+  //     let file = {
+  //       size: res.size,
+  //       path,
+  //       fileType,
+  //       name
+  //     }
+  //     console.log('根据需求构造的数据', file)
+  //   }
+  // })
+
+  // #endif
+}
+// 处理选择的文件
+const handleFile = async (fileData) => {
+
+  try {
+
+    const tempPath = await base64ToTempFile(
+      fileData.filePath,
+      fileData.name
+    )
+
+    const file = {
+      name: fileData.name,
+      size: fileData.size,
+      type: fileData.type,
+
+      // 关键
+      path: tempPath,
+
+      status: 'ready'
+    }
+
+    console.log(file, 121212);
+
+
+    tempFileQueue.value = [file]
+
+    emit('select', {
+      tempFiles: [file]
+    })
+
+    if (props.autoUpload) {
+      startUpload()
+    }
+
+  } catch (e) {
 
+    uni.showToast({
+      title: '文件处理失败',
+      icon: 'none'
+    })
+
+    console.error(e)
+  }
+}
 const startUpload = async () => {
+
   const files = tempFileQueue.value
+
   tempFileQueue.value = []
-  for (const file of files) await uploadFile(file)
+
+  for (const file of files) {
+
+    // APP base64
+    if (file.base64) {
+      await uploadBase64(file)
+    }
+
+    // H5 正常文件
+    else {
+      await uploadFile(file)
+    }
+  }
 }
 
 const uploadFile = (fileItem) => {
@@ -386,6 +485,13 @@ const uploadFile = (fileItem) => {
     innerFileList.value.push({ ...fileItem, status: 'uploading', progress: 0 })
     const index = innerFileList.value.length - 1
     const url = props.action || config.Host80 + props.uploadUrl
+    console.log({
+      url,
+      filePath: fileItem.path,
+      name: props.uploadName,
+      header: { 'Access-Token': userToken.value, ...props.uploadHeaders },
+      formData: props.uploadData
+    }, 100000);
 
     const task = uni.uploadFile({
       url,
@@ -395,7 +501,20 @@ const uploadFile = (fileItem) => {
       formData: props.uploadData,
 
       success: (res) => {
-        const result = props.responseHandler(res.data)
+        const result = typeof props.responseHandler === 'function'
+          ? props.responseHandler(res.data)
+          : {
+            success: typeof res.data === 'string'
+              ? JSON.parse(res.data).code === 200
+              : res.data.code === 200,
+            path: typeof res.data === 'string'
+              ? (JSON.parse(res.data).data?.path || JSON.parse(res.data).data)
+              : (res.data.data?.path || res.data.data),
+            message: typeof res.data === 'string'
+              ? JSON.parse(res.data).msg
+              : res.data.msg
+          }
+
         if (result.success) {
           innerFileList.value[index].progress = 100
           innerFileList.value[index].status = 'success'
@@ -404,8 +523,8 @@ const uploadFile = (fileItem) => {
           emit('success', innerFileList.value[index])
         } else {
           innerFileList.value[index].status = 'error'
-          uni.showToast({ title: result.message, icon: 'error' })
-          emit('fail', result.message)
+          uni.showToast({ title: result.message || '上传失败', icon: 'error' })
+          emit('fail', result.message || '上传失败')
         }
         resolve(result)
       },
@@ -425,6 +544,169 @@ const uploadFile = (fileItem) => {
   })
 }
 
+const uploadBase64 = (fileItem) => {
+  return new Promise((resolve) => {
+    innerFileList.value.push({
+      ...fileItem,
+      status: 'uploading',
+      progress: 0
+    })
+
+    const index =
+      innerFileList.value.length - 1
+
+    const url =
+      props.action ||
+      config.Host80 + props.uploadUrl
+
+    uni.request({
+
+      url,
+
+      method: 'POST',
+
+      header: {
+        'Content-Type': 'application/json',
+        'Access-Token': userToken.value,
+        ...props.uploadHeaders
+      },
+
+      data: {
+        file: fileItem.base64,
+        fileName: fileItem.name,
+        fileType: fileItem.type,
+        ...props.uploadData
+      },
+
+      success: (res) => {
+
+        const result =
+          typeof props.responseHandler === 'function'
+            ? props.responseHandler(res.data)
+            : {
+              success: res.data.code === 200,
+              path: res.data.data?.path || res.data.data,
+              message: res.data.msg
+            }
+
+        if (result.success) {
+
+          innerFileList.value[index].progress = 100
+          innerFileList.value[index].status = 'success'
+
+          // 这里继续用后端返回URL
+          innerFileList.value[index].url =
+            config.Host05 + result.path
+
+          innerFileList.value[index].path =
+            result.path
+
+          emit(
+            'success',
+            innerFileList.value[index]
+          )
+
+        } else {
+
+          innerFileList.value[index].status = 'error'
+
+          uni.showToast({
+            title: result.message || '上传失败',
+            icon: 'none'
+          })
+
+          emit('fail', result.message)
+        }
+
+        resolve(result)
+      },
+
+      fail: () => {
+
+        innerFileList.value[index].status = 'error'
+
+        uni.showToast({
+          title: '上传失败',
+          icon: 'none'
+        })
+
+        emit('fail', '网络异常')
+
+        resolve(null)
+      }
+    })
+  })
+}
+
+
+const base64ToTempFile = (base64, fileName = 'file.png') => {
+
+  return new Promise((resolve, reject) => {
+
+    const matches = base64.match(/^data:(.+);base64,(.+)$/)
+
+    if (!matches) {
+      reject('base64格式错误')
+      return
+    }
+
+    const base64Data = matches[2]
+
+    const filePath =
+      `${plus.io.convertLocalFileSystemURL('_doc/')}${Date.now()}_${fileName}`
+
+    plus.io.resolveLocalFileSystemURL(
+      '_doc/',
+      (entry) => {
+
+        entry.getFile(
+          `${Date.now()}_${fileName}`,
+          { create: true },
+
+          (fileEntry) => {
+
+            fileEntry.createWriter((writer) => {
+
+              writer.onwrite = () => {
+                resolve(fileEntry.toLocalURL())
+              }
+
+              writer.onerror = reject
+
+              const bitmap = new plus.nativeObj.Bitmap()
+
+              bitmap.loadBase64Data(
+                base64,
+
+                () => {
+
+                  bitmap.save(
+                    fileEntry.toLocalURL(),
+
+                    {},
+
+                    () => {
+                      resolve(fileEntry.toLocalURL())
+                    },
+
+                    reject
+                  )
+                },
+
+                reject
+              )
+            })
+          },
+
+          reject
+        )
+      },
+
+      reject
+    )
+  })
+}
+
 const reUploadFile = (index) => {
   const file = innerFileList.value[index]
   uploadFile(file)

+ 4 - 4
config/index.ts

@@ -1,14 +1,14 @@
 // #ifdef H5
 let [p, h] = [window.location.protocol, window.location.host];
 let isIP = /^\d+\.\d+\.\d+\.\d+:\d+$/.test(h);
-// let [ho, dt] = isIP ? ['44a5c8109e4', 'com'] : h.split('.').slice(-2);
-let [ho, dt] = isIP ? ['cwgvu', 'club'] : h.split('.').slice(-2);
+let [ho, dt] = isIP ? ['44a5c8109e4', 'com'] : h.split('.').slice(-2);
+// let [ho, dt] = isIP ? ['cwgvu', 'club'] : h.split('.').slice(-2);
 let ht = p == 'http:' ? 'https:' : p;
 console.log(ho, dt, ht, 1009);
 // #else
 let ht = 'https:';
-let ho = 'cwgvu'; // 默认主域名或可根据实际APP环境配置
-let dt = 'club'; // 默认域名后缀
+let ho = '44a5c8109e4'; // 默认主域名或可根据实际APP环境配置
+let dt = 'com'; // 默认域名后缀
 // #endif
 
 const config = {

+ 2 - 1
pages/customer/components/CheckPopup.vue

@@ -34,6 +34,7 @@
 <script setup>
 import { computed, ref } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { openExternalUrl } from '@/utils/openExternalUrl'
 
 const props = defineProps({
   visible: { type: Boolean, default: false },
@@ -59,7 +60,7 @@ const toHistory = () => {
 }
 // 去支付
 const GoPayBtn = () => {
-  window.open(props.goPayLink)
+  openExternalUrl(props.goPayLink)
 }
 // 复制分享链接
 const CopyShareLink = (link) => {

+ 30 - 22
pages/customer/deposit.vue

@@ -70,7 +70,7 @@
                                                         WireTransferAccount.bankUname || '--'
                                                     }}</text></view>
                                             <view class="row"><text class="label">{{ t('Custom.Deposit.bankName')
-                                            }}</text><text class="content">{{ WireTransferAccount.bankName
+                                                    }}</text><text class="content">{{ WireTransferAccount.bankName
                                                         ||
                                                         '--'
                                                     }}</text></view>
@@ -80,7 +80,7 @@
                                                         WireTransferAccount.bankCardNum || '--'
                                                     }}</text></view>
                                             <view class="row"><text class="label">{{ t('Custom.Deposit.bankAddr')
-                                            }}</text><text class="content">{{ WireTransferAccount.bankAddr
+                                                    }}</text><text class="content">{{ WireTransferAccount.bankAddr
                                                         ||
                                                         '--'
                                                     }}</text></view>
@@ -90,7 +90,7 @@
                                                         WireTransferAccount.swiftCode || '--'
                                                     }}</text></view>
                                             <view class="row"><text class="label">{{ t('Custom.Deposit.bankCode')
-                                            }}</text><text class="content">{{ WireTransferAccount.bankCode
+                                                    }}</text><text class="content">{{ WireTransferAccount.bankCode
                                                         ||
                                                         '--'
                                                     }}</text></view>
@@ -143,7 +143,7 @@
                                             <view class="label-with-icon">
                                                 <text>{{ t('Custom.Deposit.EstimatedAmount') + '(' +
                                                     `${channelData.transformCurrency || channelData.currency}` + ')'
-                                                }}</text>
+                                                    }}</text>
                                                 <uni-tooltip placement="right">
                                                     <!-- <view class="help-icon">ⓘ</view> -->
                                                     <cwg-icon name="icon_about us" :size="16" />
@@ -201,16 +201,16 @@
                                                 <checkbox :checked="params.agree4"
                                                     @click="params.agree4 = !params.agree4" />
                                                 <text>{{ t('news_add_field1.activitiesNZ.itemDeposit1')
-                                                }}</text>
+                                                    }}</text>
                                             </label>
                                             <view style="line-height: 1.5; font-size: 14px">
                                                 <text>{{ t('news_add_field1.activitiesNZ.itemDeposit2')
-                                                }}</text>
+                                                    }}</text>
                                                 <text class="clause crm-cursor" @click="dialogClauseNZ = true">{{
                                                     t('news_add_field1.activitiesNZ.itemDeposit3')
-                                                }}</text>
+                                                    }}</text>
                                                 <text>{{ t('news_add_field1.activitiesNZ.itemDeposit4')
-                                                }}</text>
+                                                    }}</text>
                                             </view>
                                         </uni-forms-item>
 
@@ -223,15 +223,15 @@
                                                 <checkbox :checked="params.agree5"
                                                     @click="params.agree5 = !params.agree5" />
                                                 <text>{{ t('news_add_field1.activitiesNZTwo.itemDeposit1')
-                                                }}</text>
+                                                    }}</text>
                                             </label>
                                             <view style="line-height: 1.5; font-size: 14px">
                                                 <text>{{ t('news_add_field1.activitiesNZTwo.itemDeposit2')
-                                                }}</text>
+                                                    }}</text>
                                                 <text class="clause crm-cursor" @click="dialogClauseNZTwo = true">{{
                                                     t('news_add_field1.activitiesNZTwo.itemDeposit3') }}</text>
                                                 <text>{{ t('news_add_field1.activitiesNZTwo.itemDeposit4')
-                                                }}</text>
+                                                    }}</text>
                                             </view>
                                         </uni-forms-item>
 
@@ -314,7 +314,7 @@
 
                                     <button class="btn btn-dark waves-effect waves-light" @click="submitConfirm">{{
                                         t('Btn.Submit')
-                                    }}</button>
+                                        }}</button>
 
                                 </view>
 
@@ -407,6 +407,7 @@ import CwgCheckConfirmPopup from './components/DepositCheckConfirmPopup.vue'
 import VietnamNoticePopup from './components/VietnamNoticePopup.vue'
 import NewYear24Popup from './components/NewYear24Popup.vue'
 import DigitalPayConfirmPopup from './components/digitalPayConfirmPopup.vue'
+import { openExternalUrl } from '@/utils/openExternalUrl'
 // 假设原有导入路径保持不变
 import tool from "@/global/tool"
 import { userToken } from '@/composables/config'
@@ -1172,6 +1173,8 @@ const confirmDigitalPayModal = (e) => {
     dialogDigitalPayConfirm.value = false;
     imageUrl.value = e.voucherUrl || e.image || '';
     hashCode.value = e.hashCode || '';
+    console.log(e, 2222);
+
     openDontActive();
 }
 // 关闭数字支付确认弹窗
@@ -1248,6 +1251,7 @@ const submit = async () => {
                 phone: channelData.confirmPhone ? dialogCheckConfirm_form.confirmPhone : null,
                 ip: uni.getStorageSync("CLIENT")
             })
+            console.log(res, 121212);
             dialogCheckWait.value = false
             if (res.code == Code.StatusOK) {
                 imageUrl1.value = ""
@@ -1276,6 +1280,7 @@ const submit = async () => {
                 promoCode: params.promoCode,
                 ip: uni.getStorageSync("CLIENT")
             })
+            console.log(res, 121212);
             dialogCheckWait.value = false
             if (res.code == Code.StatusOK) {
                 dialogCheckOK.value = true
@@ -1308,18 +1313,20 @@ const submit = async () => {
                 ...form
             })
             dialogCheckWait.value = false
+            console.log(res, 121212);
+
             if (res.code == Code.StatusOK) {
                 if (res.data && (res.data.type == 3 || res.data.type == 4 || res.data.type == 6 || res.data.type == 7)) {
                     dealResult(res.data)
                 } else {
                     if (res.data.type == 1) {
                         goPayLink.value = res.data.result
-                        window.open(res.data.result)
+                        openExternalUrl(res.data.result)
                     } else if (res.data.type == 2) {
                         let token = userToken.value
                         token = tool.tokenReplace(token)
                         goPayLink.value = `${Host04}/finance/deposit/get?serial=${res.data.result}&Access-Token=${token}`
-                        window.open(goPayLink.value)
+                        openExternalUrl(goPayLink.value)
                     } else if (res.data.type == 5) {
                         $pigeon.MessageConfirm(
                             t("PersonalManagement.Label.item2"),
@@ -1331,18 +1338,18 @@ const submit = async () => {
                                 goPayLink.value = res.data.result
                                 dialogCheck.value = true
                                 dialogVisible.value = true
-                                window.open(res.data.result)
+                                openExternalUrl(res.data.result)
                             }
                         )
                     } else if (res.data.type == 8) {
                         goPayLink.value = Host04 + res.data.result
-                        window.open(goPayLink.value)
+                        openExternalUrl(goPayLink.value)
                     } else if (res.data.type == 9) {
                         goPayLink.value = setCardUrl(res.data.result)
-                        window.open(goPayLink.value)
+                        openExternalUrl(goPayLink.value)
                     } else if (res.data.type == 10) {
                         goPayLink.value = Host80 + '/pay/onchainpay.html?params=' + res.data.result
-                        window.open(goPayLink.value)
+                        openExternalUrl(goPayLink.value)
                     }
                     if (res.data.type != 5) {
                         dialogCheck.value = true
@@ -1358,6 +1365,7 @@ const submit = async () => {
             }
         }
     } catch (error) {
+        console.log(error, 12);
         dialogCheckWait.value = false
         flag.value = false
         dialogCheckOK.value = true
@@ -1418,12 +1426,12 @@ const submitDealResult = async () => {
         } else {
             if (res.data.type == 1) {
                 goPayLink.value = res.data.result
-                window.open(res.data.result)
+                openExternalUrl(res.data.result)
             } else if (res.data.type == 2) {
                 let token = userToken.value
                 token = tool.tokenReplace(token)
                 goPayLink.value = `${Host04}/finance/deposit/get?serial=${res.data.result}&Access-Token=${token}`
-                window.open(goPayLink.value)
+                openExternalUrl(goPayLink.value)
             } else if (res.data.type == 5) {
                 $pigeon.MessageConfirm(
                     t("PersonalManagement.Label.item2"),
@@ -1435,12 +1443,12 @@ const submitDealResult = async () => {
                         goPayLink.value = res.data.result
                         dialogCheck.value = true
                         dialogVisible.value = true
-                        window.open(res.data.result)
+                        openExternalUrl(res.data.result)
                     }
                 )
             } else if (res.data.type == 8) {
                 goPayLink.value = Host04 + res.data.result
-                window.open(goPayLink.value)
+                openExternalUrl(goPayLink.value)
             } else if (res.data.type == 9) {
                 dialogCheckOK.value = true
                 dialogVisible.value = true

+ 2 - 0
uni_modules/cop-chooseFile/changelog.md

@@ -0,0 +1,2 @@
+## 1.0.0(2026-01-11)
+初始化第一版

+ 258 - 0
uni_modules/cop-chooseFile/components/cop-chooseFile/cop-chooseFile.vue

@@ -0,0 +1,258 @@
+<template>
+    <!-- renderjs 触发器:通过改变 prop 来触发文件选择 -->
+    <view 
+        style="position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0;"
+        :change:triggerPicker="renderJS.onTriggerChange"
+        :triggerPicker="triggerFlag"
+        :change:acceptProp="renderJS.acceptChanged"
+        :acceptProp="accept">
+    </view>
+</template>
+<script>
+export default {
+    props: {
+        // 触发标记(外部改变此值可触发文件选择)
+        trigger: {
+            type: Number,
+            default: 0
+        },
+        // 接受的文件类型
+        accept: {
+            type: String,
+            default: '*'
+        }
+    },
+    data() {
+        return {
+            triggerFlag: 0
+        }
+    },
+    watch: {
+        // 监听外部 trigger 的变化
+        trigger(newVal) {
+            if (newVal) {
+                this.triggerFlag = newVal
+            }
+        }
+    },
+    onLoad(res) {},
+    methods: {
+        // plus.io选择文件
+        // 选择完文件后,拿到的是base64字符串,转成对应的数据
+        parseJSONData(base64Str) {
+            console.log('选择的文件base64Str', base64Str)
+            let jsonStr = this.convertBase64ToUTF8(base64Str)
+            if (base64Str.includes('application/json')) {
+                let jsonData = JSON.parse(jsonStr)
+                this.$emit('readJSONFinish', { jsonData })
+            } else {
+                this.$emit('readJSONFinish', { jsonStr })
+            }
+        },
+
+        convertBase64ToUTF8(base64Str) {
+            let base64Content = atob(base64Str.split(',')[1])
+            base64Content = base64Content
+                .split('')
+                .map(function (c) {
+                    return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
+                })
+                .join('')
+            try {
+                let jsonStr = decodeURIComponent(base64Content)
+                return jsonStr
+            } catch (error) {
+                console.error('读取失败', error)
+                uni.showToast({
+                    title: '读取失败,不支持此格式文件',
+                    icon: 'none'
+                })
+            }
+        },
+
+        async receiveRenderFile(result) {
+            console.log('receiveRenderFile 被调用,文件信息:', result)
+            
+            // 直接 emit 原始文件数据(包含 base64),不进行转换
+            this.$emit('receiveRenderFile', result)
+            
+            // #ifdef APP-PLUS
+            // 如果需要本地路径,可以进行转换(可选)
+            // const fileUrl = await this.base64toPath(result.filePath, result.name)
+            // this.fileName = fileUrl.relativePath
+            // this.filePath = fileUrl.localAbsolutePath
+            // #endif
+            // #ifdef H5
+            this.fileName = result.name
+            this.filePath = result.filePath
+            // #endif
+        },
+
+        //将base64转成路径
+        async base64toPath(base64, attachName) {
+            console.log('base64开始转化成文件')
+            let _that = this
+            return new Promise(function (resolve, reject) {
+                const filePath = `_doc/yourFilePath/${attachName}`
+                plus.io.resolveLocalFileSystemURL(
+                    '_doc',
+                    function (entry) {
+                        entry.getDirectory(
+                            'yourFilePath',
+                            {
+                                create: true,
+                                exclusive: false
+                            },
+                            function (entry) {
+                                entry.getFile(
+                                    attachName,
+                                    {
+                                        create: true,
+                                        exclusive: false
+                                    },
+                                    function (entry) {
+                                        entry.createWriter(function (writer) {
+                                            writer.onwrite = function (res) {
+                                                console.log('base64转化文件完成')
+                                                const obj = {
+                                                    relativePath: filePath,
+                                                    localAbsolutePath:
+                                                        plus.io.convertLocalFileSystemURL(filePath)
+                                                }
+                                                resolve(obj)
+                                            }
+                                            writer.onerror = reject
+                                            writer.seek(0)
+                                            writer.writeAsBinary(
+                                                _that.getSymbolAfterString(base64, ',')
+                                            )
+                                        }, reject)
+                                    },
+                                    reject
+                                )
+                            },
+                            reject
+                        )
+                    },
+                    reject
+                )
+            })
+        },
+        // 取某个符号后面的字符
+        getSymbolAfterString(val, symbolStr) {
+            if (val == undefined || val == null || val == '') {
+                return ''
+            }
+            val = val.toString()
+            const index = val.indexOf(symbolStr)
+            if (index != -1) {
+                val = val.substring(index + 1, val.length)
+                return val
+            } else {
+                return val
+            }
+        }
+    }
+}
+</script>
+<script module="renderJS" lang="renderjs">
+export default {
+	data() {
+		return {
+			acceptType: '*'
+		}
+	},
+	mounted() {
+		// renderjs 的 mounted 不接收参数,acceptType 通过 acceptChanged 方法初始化
+		console.log('cop-chooseFile renderJS mounted')
+	},
+	methods: {
+		// 监听触发标记的变化
+		onTriggerChange(newVal, oldVal, ownerVm, ins) {
+			console.log('cop-chooseFile: 触发标记改变', newVal)
+			
+			if (newVal && newVal !== oldVal) {
+				this.createFileInputDom(null, ownerVm)
+			}
+		},
+		
+		// 接收 accept 属性的变化
+		acceptChanged(newVal) {
+			this.acceptType = newVal || '*'
+		},
+		
+		createFileInputDom(e, ownerVm) {
+			console.log('cop-chooseFile: 开始选择文件')
+			
+			let fileInput = document.createElement('input')
+			fileInput.setAttribute('type', 'file')
+			fileInput.setAttribute('accept', this.acceptType)
+			fileInput.style.display = 'none'
+			document.body.appendChild(fileInput)
+			
+			fileInput.click()
+			fileInput.addEventListener('change', e => {
+				let file = e.target.files[0]
+				
+				if (file) {
+					console.log('cop-chooseFile: 选择了文件', file.name, file.size)
+					
+					// #ifdef APP-PLUS
+					let reader = new FileReader();
+					reader.readAsDataURL(file);
+					reader.onload = function(event) {
+						const base64Str = event.target.result; // 文件的base64
+						console.log('cop-chooseFile: 文件读取完成')
+						
+						// 先调用 receiveRenderFile 传递原始文件信息(用于所有文件类型)
+						ownerVm.callMethod('receiveRenderFile', {
+							name: file.name,
+							filePath: base64Str,
+							size: file.size,
+							type: file.type
+						})
+						
+						// 只对文本/JSON 文件调用 parseJSONData
+						const isTextFile = file.type.includes('text') || file.type.includes('json') || 
+						                   file.name.endsWith('.txt') || file.name.endsWith('.json')
+						
+						if (isTextFile) {
+							try {
+								ownerVm.callMethod('parseJSONData', base64Str)
+							} catch(e) {
+								console.log('parseJSONData failed:', e.message)
+							}
+						}
+					}
+					// #endif
+
+					// #ifdef H5
+					// 如果需要得到文件的本地路径,可以通过下面方法
+					const filePath = URL.createObjectURL(file)
+					ownerVm.callMethod('receiveRenderFile', {
+						name: file.name,
+						filePath: filePath,
+						size: file.size,
+						type: file.type
+					})
+					// #endif
+				} else {
+					console.log('cop-chooseFile: 未选择文件')
+				}
+				
+				// 清理 input 元素
+				setTimeout(function() {
+					if (fileInput.parentNode) {
+						document.body.removeChild(fileInput)
+						console.log('cop-chooseFile: input 已清理')
+					}
+				}, 100)
+			})
+		}
+	}
+}
+</script>
+<style scoped>
+/* 无样式,组件为隐藏触发器 */
+</style>
+

+ 122 - 0
uni_modules/cop-chooseFile/readme.md

@@ -0,0 +1,122 @@
+# cop-chooseFile 文件选择组件
+
+基于 renderjs 的文件选择组件,支持触发模式,可选择任意类型文件。
+
+## 特点
+
+- ✅ 基于 renderjs,稳定可靠
+- ✅ 支持触发模式,无需显示按钮
+- ✅ 支持自定义接受的文件类型
+- ✅ 返回 base64 格式文件数据
+- ✅ APP 和 H5 双端支持
+
+## 使用方法
+
+### 基本使用
+
+```vue
+<template>
+  <view>
+    <!-- 自定义触发按钮 -->
+    <button @click="selectFile">选择文件</button>
+    
+    <!-- 隐藏的文件选择组件 -->
+    <cop-chooseFile 
+      :trigger="triggerFlag"
+      accept="*"
+      @receiveRenderFile="handleFile">
+    </cop-chooseFile>
+  </view>
+</template>
+
+<script>
+import copChooseFile from '@/uni_modules/cop-chooseFile/components/cop-chooseFile/cop-chooseFile.vue'
+
+export default {
+  components: {
+    copChooseFile
+  },
+  data() {
+    return {
+      triggerFlag: 0
+    }
+  },
+  methods: {
+    // 点击按钮触发文件选择
+    selectFile() {
+      this.triggerFlag = Date.now()
+    },
+    
+    // 处理选择的文件
+    handleFile(fileData) {
+      console.log('文件名:', fileData.name)
+      console.log('文件大小:', fileData.size)
+      console.log('文件类型:', fileData.type)
+      console.log('文件base64:', fileData.filePath)
+    }
+  }
+}
+</script>
+```
+
+### 选择特定类型文件
+
+```vue
+<!-- 只选择图片 -->
+<cop-chooseFile 
+  :trigger="triggerFlag"
+  accept="image/*"
+  @receiveRenderFile="handleFile">
+</cop-chooseFile>
+
+<!-- 只选择音频 -->
+<cop-chooseFile 
+  :trigger="triggerFlag"
+  accept="audio/*,.mp3,.wav"
+  @receiveRenderFile="handleFile">
+</cop-chooseFile>
+
+<!-- 只选择文档 -->
+<cop-chooseFile 
+  :trigger="triggerFlag"
+  accept=".pdf,.doc,.docx,.txt"
+  @receiveRenderFile="handleFile">
+</cop-chooseFile>
+```
+
+## API
+
+### Props
+
+| 参数 | 类型 | 默认值 | 说明 |
+|-----|------|--------|------|
+| trigger | Number | 0 | 触发标记,改变此值触发文件选择 |
+| accept | String | '*' | 接受的文件类型 |
+
+### Events
+
+| 事件名 | 说明 | 回调参数 |
+|--------|------|----------|
+| receiveRenderFile | 文件选择完成 | fileData: { name, filePath, size, type } |
+
+### fileData 参数说明
+
+- `name`: 文件名
+- `filePath`: 文件路径(APP 端为 base64,H5 端为 blob URL)
+- `size`: 文件大小(字节)
+- `type`: 文件 MIME 类型
+
+## 注意事项
+
+1. 组件是隐藏的,不会在页面上显示
+2. 通过改变 `trigger` prop 的值来触发文件选择
+3. APP 端返回的是 base64 格式,可能较大,注意性能
+4. H5 端返回的是 blob URL,需要读取后才能获取 base64
+
+## 更新日志
+
+### 1.0.0 (2026-01-07)
+- 初始版本
+- 支持触发模式文件选择
+- 支持 APP 和 H5 双端
+