Просмотр исходного кода

✨ feat(ruidu): 日报上传附件,查看显示所有

Zimo 1 месяц назад
Родитель
Сommit
4bd5bc2a04

BIN
android/simpleDemo/libs/android-gif-drawable-1.2.28.aar


BIN
android/simpleDemo/libs/android-gif-drawable-1.2.29.aar


BIN
android/simpleDemo/libs/breakpad-build-release.aar


BIN
android/simpleDemo/libs/lib.5plus.base-release.aar


BIN
android/simpleDemo/libs/oaid_sdk_1.0.25.aar


+ 2 - 2
config/env.prod.js

@@ -1,8 +1,8 @@
 // 生产环境配置
 export default {
 	// apiUrl: 'http://192.168.1.63:8888',  
-	// apiUrl: 'https://iot.deepoil.cc',	//测试
-	apiUrl: 'https://aims.deepoil.cc', //正式
+	apiUrl: 'https://iot.deepoil.cc',	//测试
+	// apiUrl: 'https://aims.deepoil.cc', //正式
 	apiUrlSuffix: '/admin-api',
 	// 其他开发环境配置...  
 	corpId: 'dingbe7f9a7e8cffa2bd35c2f4657eb6378f', //钉钉微应用 企业的CorpID - 正式环境

+ 0 - 995
pages/ruiDu/compontents/report-form-copy.vue

@@ -1,995 +0,0 @@
-<template>
-  <scroll-view scroll-y="true" class="report-form">
-    <!-- 日报详情 - 日报信息 -->
-    <view class="report-form-content form-content">
-      <uni-forms
-        ref="reportFormRef"
-        labelWidth="140px"
-        :modelValue="formData"
-        :rules="formDataRules"
-        err-show-type="toast">
-        <!-- 时间节点 -->
-        <uni-forms-item class="form-item" :label="`${$t('ruiDu.timeNode')}:`" :required="isRequired">
-          <view
-            class="time-range-container flex-row align-center justify-end"
-            @click="props.formDisable ? '' : handleClickTimeRange()">
-            <view class="time-range-item" v-if="formData.startTime && formData.endTime">
-              {{ formData.startTime }} 至 {{ formData.endTime }}
-            </view>
-            <view class="time-range-item" v-else>
-              {{ selectPlaceholder }}
-            </view>
-          </view>
-        </uni-forms-item>
-        <!-- 施工状态 -->
-        <uni-forms-item
-          class="form-item"
-          :label="`${$t('ruiDu.constructionStatus')}:`"
-          :required="isRequired"
-          name="rdStatus">
-          <uni-data-select
-            class="form-item-select align-center"
-            :clear="false"
-            :align="'right'"
-            :localdata="rdStatusRange"
-            :placeholder="selectPlaceholder"
-            :disabled="props.formDisable"
-            v-model="formData.rdStatus">
-          </uni-data-select>
-        </uni-forms-item>
-        <!-- 施工设备 -->
-        <uni-forms-item class="form-item" :label="`${$t('ruiDu.constructionEquipment')}:`">
-          <view class="flex-col">
-            <uni-row class="flex-row align-center justify-end">
-              <uni-col :span="6" class="flex-col align-center justify-end">
-                <button
-                  class="mini-btn form-item-btn align-center justify-center"
-                  type="primary"
-                  :disabled="props.formDisable"
-                  v-if="!props.formDisable"
-                  @click="handleClickSelectDevice">
-                  {{ $t('device.selectDevice') }}
-                </button>
-              </uni-col>
-            </uni-row>
-            <uni-row>
-              <uni-easyinput
-                style="text-align: right"
-                type="textarea"
-                :autoHeight="true"
-                :inputBorder="false"
-                :clearable="false"
-                :styles="{ disableColor: '#fff' }"
-                :placeholder="$t('ruiDu.unselectedEquipment')"
-                :disabled="true"
-                v-model="selectedEquipmentNames" />
-            </uni-row>
-          </view>
-        </uni-forms-item>
-        <!-- 未选择设备 -->
-        <uni-forms-item class="form-item" :label="`${$t('ruiDu.unselectedEquipment')}:`">
-          <uni-easyinput
-            style="text-align: right"
-            type="textarea"
-            :autoHeight="true"
-            :inputBorder="false"
-            :clearable="false"
-            :styles="{ disableColor: '#fff' }"
-            :placeholder="' '"
-            :disabled="true"
-            v-model="unselectedEquipmentNames" />
-        </uni-forms-item>
-        <!-- 施工工艺 -->
-        <uni-forms-item
-          class="form-item"
-          :label="`${$t('ruiDu.constructionProcess')}:`"
-          :required="isRequired"
-          name="techniqueIds">
-          <uni-data-select
-            ref="uniDataSelect"
-            mode="underline"
-            placement="bottom"
-            :multiple="true"
-            :clear="false"
-            :localdata="techniqueRange"
-            :disabled="props.formDisable"
-            v-model="formData.techniqueIds">
-            <template v-slot:selected="{ selectedItems }" class="form-item">
-              <view class="slot-box flex-row align-center justify-end">
-                <view
-                  v-for="item in selectedItems"
-                  :key="item.value"
-                  class="slot-content-item selected align-center justify-start">
-                  {{ item.text }}
-                  <uni-icons
-                    type="close"
-                    size="18"
-                    color="#909399"
-                    v-if="!props.formDisable"
-                    @click="removeSelectedItem(item.value)"></uni-icons>
-                </view>
-                <view v-if="selectedItems.length == 0" class="slot-content-item align-center justify-start">
-                  {{ selectPlaceholder }}
-                </view>
-              </view>
-            </template>
-            <template v-slot:option="{ item, itemSelected }">
-              <view class="slot-item">
-                <uni-list-item class="slot-list-item flex-row align-center justify-between">
-                  <template v-slot:body>
-                    <text class="slot-item-text justify-start" :style="{ color: itemSelected ? '#007aff' : '#303133' }">
-                      {{ item.text }}
-                    </text>
-                  </template>
-                  <template v-slot:footer>
-                    <uni-icons
-                      class="align-center justify-center"
-                      v-if="itemSelected"
-                      type="checkmarkempty"
-                      size="20"
-                      color="#007aff"></uni-icons>
-                  </template>
-                </uni-list-item>
-              </view>
-            </template>
-            <!-- 可以拦截点击事件之后自定义
-					<template v-slot:option="{item,itemSelected}">
-						<view @click.stop>
-							<uni-list-item showSwitch :switchChecked="itemSelected" :title="item.text" :note="item.value+''"
-								:disabled="item.disable" @switchChange="switchChange($event,item)"></uni-list-item>
-						</view>
-					</template> -->
-            <template v-slot:empty>
-              <view class="empty-box">
-                <view>{{ $t('common.noData') }}</view>
-              </view>
-            </template>
-          </uni-data-select>
-        </uni-forms-item>
-
-        <!--当日生产动态 -->
-        <uni-forms-item
-          class="form-item"
-          :label="`${$t('ruiDu.dailyProductionDynamic')}:`"
-          :required="isRequired"
-          name="productionStatus">
-          <uni-easyinput
-            style="text-align: right"
-            type="textarea"
-            :autoHeight="true"
-            :inputBorder="false"
-            :clearable="false"
-            :styles="{ disableColor: '#fff' }"
-            :placeholder="inputPlaceholder"
-            :disabled="props.formDisable"
-            v-model="formData.productionStatus" />
-        </uni-forms-item>
-        <!-- 下步工作计划 -->
-        <uni-forms-item class="form-item" :label="`${$t('ruiDu.nextWorkPlan')}:`" name="nextPlan">
-          <uni-easyinput
-            style="text-align: right"
-            type="textarea"
-            :autoHeight="true"
-            :inputBorder="false"
-            :clearable="false"
-            :styles="{ disableColor: '#fff' }"
-            :placeholder="inputPlaceholder"
-            :disabled="props.formDisable"
-            v-model="formData.nextPlan" />
-        </uni-forms-item>
-        <!-- 外租设备 -->
-        <uni-forms-item class="form-item" :label="`${$t('ruiDu.externalRentalEquipment')}:`" name="externalRental">
-          <uni-easyinput
-            style="text-align: right"
-            type="textarea"
-            :autoHeight="true"
-            :inputBorder="false"
-            :clearable="false"
-            :styles="{ disableColor: '#fff' }"
-            :placeholder="inputPlaceholder"
-            :disabled="props.formDisable"
-            v-model="formData.externalRental" />
-        </uni-forms-item>
-        <!-- 故障情况 -->
-        <uni-forms-item class="form-item" :label="`${$t('ruiDu.faultSituation')}:`" name="malfunction">
-          <uni-easyinput
-            style="text-align: right"
-            type="textarea"
-            :autoHeight="true"
-            :inputBorder="false"
-            :clearable="false"
-            :styles="{ disableColor: '#fff' }"
-            :placeholder="inputPlaceholder"
-            :disabled="props.formDisable"
-            v-model="formData.malfunction" />
-        </uni-forms-item>
-        <!-- 故障误工H -->
-        <uni-forms-item class="form-item" :label="`${$t('ruiDu.faultDowntimeH')}:`" name="faultDowntime">
-          <uni-easyinput
-            class="digit-item"
-            type="digit"
-            :inputBorder="false"
-            :clearable="true"
-            :styles="{ disableColor: '#fff' }"
-            :placeholder="inputPlaceholder"
-            :disabled="props.formDisable"
-            v-model="formData.faultDowntime" />
-        </uni-forms-item>
-        <!-- 附件 -->
-        <uni-forms-item class="form-item" :label="`${$t('ruiDu.attachment')}:`" :required="false" name="attachments">
-          <uni-file-picker
-            file-mediatype="all"
-            :clear="false"
-            :limit="9"
-            :auto-upload="false"
-            :readonly="props.formDisable"
-            v-model="attachmentsFileList"
-            @select="uploadFiles"
-            @delete="deleteFiles"
-            v-if="!props.formDisable">
-            <view class="file-picker-container flex-row align-end justify-end">
-              <view class="file-size-limit">
-                {{ $t('ruiDu.fileSizeLimit') }}
-              </view>
-              <button type="primary" size="mini" class="file-picker-btn">
-                {{ $t('ruiDu.selectFile') }}
-              </button>
-            </view>
-          </uni-file-picker>
-          <view class="file-list flex-col justify-start" v-else>
-            <uni-row
-              class="file-item flex-row align-center justify-between"
-              v-for="(file, index) in attachmentsFileList"
-              :key="index">
-              <uni-col :span="20" class="file-name flex-row">{{ file.name }}</uni-col>
-              <uni-col :span="3" class="file-btn align-center" @click="downloadFile(file)">
-                {{ $t('operation.download') }}
-              </uni-col>
-            </uni-row>
-          </view>
-        </uni-forms-item>
-        <!--  ---------------  以下是根据施工工艺获取的工作量字段  ------------------ -->
-        <uni-forms-item
-          class="form-item"
-          v-for="(attr, index) in formData.extProperty"
-          :key="index"
-          :label="`${attr.name}(${attr.unit}):`"
-          :required="attr.required == 1 && isRequired"
-          :name="`extProperty[${index}].actualValue`">
-          <uni-easyinput
-            v-if="attr.dataType === 'double'"
-            class="digit-item"
-            type="digit"
-            :inputBorder="false"
-            :clearable="true"
-            :styles="{ disableColor: '#fff' }"
-            :placeholder="inputPlaceholder"
-            :disabled="props.formDisable"
-            v-model="attr.actualValue" />
-          <uni-easyinput
-            v-else
-            style="text-align: right"
-            :styles="{ disableColor: '#fff' }"
-            :inputBorder="false"
-            :clearable="true"
-            :placeholder="inputPlaceholder"
-            :disabled="props.formDisable"
-            v-model="attr.actualValue"
-            :type="'textarea'"></uni-easyinput>
-        </uni-forms-item>
-      </uni-forms>
-    </view>
-  </scroll-view>
-  <!-- 时间范围选择器 (时:分)-->
-  <tpf-time-range
-    ref="timeRangeRef"
-    :startTime="startTime"
-    :startDefaultTime="startDefaultTime"
-    :endTime="endTime"
-    :endDefaultTime="endDefaultTime"
-    @timeRange="timeRange"></tpf-time-range>
-  <!-- 设备选择器 (穿梭框) -->
-  <device-transfer
-    ref="deviceTransferRef"
-    :allList="reportData.selectedDevices"
-    :selected="formData.deviceIds"
-    @confirm="handleTransferChange" />
-</template>
-
-<script setup>
-  import { onLoad } from '@dcloudio/uni-app';
-  import { ref, reactive, computed, getCurrentInstance, onMounted, nextTick, watch } from 'vue';
-
-  // -------------------------- 引入api接口 start--------------------------
-  import { getRuiDuReportAttrs, updateRuiDuReport, uploadAttachmentsFile } from '@/api/ruiDu.js';
-  // -------------------------- 引入api接口 end  --------------------------
-  // --------------------------引用组件-----start---------------------------
-  import tpfTimeRange from '@/components/tpf-time-range/tpf-time-range.vue';
-  import deviceTransfer from '@/components/device-transfer/index.vue';
-  // --------------------------引用组件-----end-----------------------------
-  // --------------------------引用全局变量$t-------------------------------
-  const { appContext } = getCurrentInstance();
-  const t = appContext.config.globalProperties.$t;
-  // --------------------------引用字典项-----------------------------------
-  import { useDataDictStore } from '@/store/modules/dataDict';
-  const { getIntDictOptions, getStrDictOptions } = useDataDictStore();
-
-  // --------------------------字典项-----------------------------------
-  // 施工状态
-  const rdStatusRange = getStrDictOptions('rdStatus').map(item => {
-    return {
-      ...item,
-      text: item.label,
-    };
-  });
-  console.log('🚀 ~ 施工状态:', rdStatusRange);
-  // 施工工艺
-  const techniqueRange = getIntDictOptions('rq_iot_project_technology_rd').map(item => {
-    return {
-      ...item,
-      text: item.label,
-    };
-  });
-  console.log('🚀 ~ 施工工艺:', techniqueRange);
-
-  // -------------------------接收父组件传递的参数--------------------------
-  const props = defineProps({
-    reportId: {
-      type: String,
-      default: '',
-    },
-    reportData: {
-      type: Object,
-      default: () => {},
-    },
-    formDisable: {
-      type: Boolean,
-      default: false, // 是否禁用表单
-    },
-  });
-
-  // -------------------------- 生命周期函数 --------------------------
-  onMounted(() => {
-    console.log('🚀 ~ report-form onMounted ~ props:', props);
-    // 初始化表单数据
-    // formDataFormat();
-  });
-  onLoad(options => {
-    console.log('🚀 ~ report-form onLoad ~ options:', options);
-  });
-
-  // -------------------------- 页面变量 --------------------------
-  // 表单ref
-  const reportFormRef = ref(null);
-  // 选择占位符
-  const selectPlaceholder = computed(() => {
-    return props.formDisable ? ' ' : t('operation.PleaseSelect');
-  });
-  // 输入占位符
-  const inputPlaceholder = computed(() => {
-    return props.formDisable ? ' ' : t('operation.PleaseInput');
-  });
-  // 表单项是否必填 required
-  const isRequired = computed(() => {
-    return props.formDisable ? false : true;
-  });
-
-  // ---------------时间范围---------------
-  const timeRangeRef = ref(null);
-  const startTime = ref('00:00');
-  const startDefaultTime = ref('06:00');
-  const endTime = ref('24:00');
-  const endDefaultTime = ref('06:00');
-  // ------------- 施工设备 ---------------
-  // 设备选择器
-  const deviceTransferRef = ref(null);
-  // 已选择的设备(名称)
-  const selectedEquipmentNames = ref('');
-  // 未选择的设备(名称)
-  const unselectedEquipmentNames = ref('');
-  // 上传的附件文件列表
-  const attachmentsFileList = ref([]);
-
-  // 表单数据
-  const formData = ref({
-    startTime: startDefaultTime.value, //时间节点 - 开始时间
-    endTime: endDefaultTime.value, //时间节点 - 结束时间
-    rdStatus: '', //施工状态
-    deviceIds: [], //施工设备
-    techniqueIds: [], //施工工艺
-    productionStatus: '', //当日生产动态
-    attachments: [], // 附件
-    extProperty: [], // 扩展属性
-  });
-  console.log('🚀 ~ formData:', formData);
-  // 表单校验规则
-  const formDataBaseRules = reactive({
-    // 时间节点 - 开始时间
-    startTime: {
-      rules: [
-        {
-          required: true,
-          errorMessage: `${t('operation.PleaseSelect')}${t('ruiDu.timeNode')}`,
-        },
-      ],
-    },
-    // 时间节点 - 结束时间
-    endTime: {
-      rules: [
-        {
-          required: true,
-          errorMessage: `${t('operation.PleaseSelect')}${t('ruiDu.timeNode')}`,
-        },
-      ],
-    },
-    // 施工状态
-    rdStatus: {
-      rules: [
-        {
-          required: true,
-          errorMessage: `${t('operation.PleaseSelect')}${t('ruiDu.constructionStatus')}`,
-        },
-      ],
-    },
-    // 施工工艺
-    techniqueIds: {
-      rules: [
-        {
-          required: true,
-          errorMessage: `${t('operation.PleaseSelect')}${t('ruiDu.constructionProcess')}`,
-        },
-      ],
-    },
-    // 当日生产动态
-    productionStatus: {
-      rules: [
-        {
-          required: true,
-          errorMessage: `${t('operation.PleaseFillIn')}${t('ruiDu.dailyProductionDynamic')}`,
-        },
-      ],
-    },
-  });
-  // 动态计算表单校验规则
-  const formDataRules = computed(() => {
-    const rules = { ...formDataBaseRules };
-    // 根据施工工艺动态添加工作量属性字段的校验规则
-    formData.value.extProperty.forEach((attr, index) => {
-      if (attr.required == 1) {
-        rules[`extProperty[${index}].actualValue`] = {
-          rules: [
-            {
-              required: true,
-              errorMessage: `${t('operation.PleaseFillIn')}${attr.name}`,
-            },
-          ],
-        };
-      }
-    });
-    console.log('生成的校验规则:', rules);
-    return rules;
-  });
-  // -------------------------- 页面方法  --------------------------
-
-  // 表单校验方法
-  const validate = async () => {
-    // 同步最新的formDataRules(解决computed延迟)
-    const latestRules = formDataRules.value;
-    await nextTick(); // 等待DOM与规则同步
-    reportFormRef.value.setRules(latestRules);
-    console.log('基础校验通过(formDataRules已生效)');
-    // 调用uni-forms的validate方法进行校验
-    return await reportFormRef.value.validate();
-  };
-  // 表单提交
-  const submitForm = async () => {
-    console.log('🚀 ~ formDataRules:', formDataRules);
-    console.log('🚀 ~ submitForm ~ formData.value:', formData.value);
-
-    const formValid = await validate();
-    if (formValid) {
-      // 校验施工工艺的工作量属性字段是否填写
-      formData.value.extProperty.forEach(attr => {
-        if (attr.required == 1 && !attr.actualValue) {
-          uni.showToast({
-            title: `${t('operation.PleaseFillIn')}${attr.name}(${attr.unit})`,
-            icon: 'none',
-          });
-          return false;
-        }
-      });
-    }
-    // 处理表单数据
-    const formDataCopy = JSON.parse(JSON.stringify(formData.value));
-    // 处理施工工艺
-    formDataCopy.extProperty.forEach(attr => {
-      if (attr.dataType === 'double') {
-        attr.actualValue = Number(attr.actualValue);
-      }
-    });
-    console.log('🚀 ~ submitForm ~ formDataCopy:', formDataCopy);
-    // 提交表单
-    updateRuiDuReport({
-      id: props.reportId,
-      companyId: props.reportData.companyId,
-      deptId: props.reportData.deptId,
-      ...formDataCopy,
-    }).then(res => {
-      // 提交成功
-      if (res.code === 0) {
-        uni.showToast({ title: t('operation.success'), icon: 'none' });
-        // 返回上一页
-        uni.navigateBack();
-      } else {
-        uni.showToast({ title: res.msg, icon: 'none' });
-      }
-    });
-  };
-  // 点击时间范围选择
-  const handleClickTimeRange = () => {
-    console.log('🚀 ~ handleClickTimeRange ~ timeRangeRef.value:', timeRangeRef.value);
-    // 打开时间范围选择器
-    timeRangeRef.value.open();
-  };
-  // 时间范围选择回调
-  const timeRange = data => {
-    console.log('🚀 ~ timeRange ~ data:', data);
-    formData.value.startTime = data[0];
-    formData.value.endTime = data[1];
-  };
-  // 初始化表单数据
-  const formDataFormat = () => {
-    // 处理时间范围
-    timeRangeFormat();
-    // 处理已选择的设备
-    if (props.reportData?.deviceIds) {
-      const { deviceIds = [] } = props.reportData || {};
-      handleEquipmentNames(deviceIds);
-    }
-
-    // 处理其他表单数据
-    // 施工状态
-    formData.value.rdStatus = props.reportData.rdStatus || ''; //施工状态
-    // 施工工艺
-    formData.value.techniqueIds = props.reportData.techniqueIds || []; //施工工艺
-    // 当日生产动态
-    formData.value.productionStatus = props.reportData.productionStatus || ''; //当日生产动态
-    // 下步工作计划
-    formData.value.nextPlan = props.reportData.nextPlan || ''; //下步工作计划
-    // 外租设备
-    formData.value.externalRental = props.reportData.externalRental || ''; //外租设备
-    // 故障情况
-    formData.value.malfunction = props.reportData.malfunction || ''; //故障情况
-    // 故障误工H
-    formData.value.faultDowntime = props.reportData.faultDowntime || ''; //故障误工H
-    // 附件
-    formData.value.attachments = props.reportData.attachments || [];
-    // 展示用的文件列表
-    attachmentsFileList.value =
-      props.reportData?.attachments?.map(item => ({
-        ...item,
-        name: item.filename,
-        url: item.filePath,
-      })) || [];
-    // 施工工艺对应的工作量属性字段
-    formData.value.extProperty = props.reportData.extProperty || [];
-    console.log('🚀 ~ formDataFormat ~ formData.value:', formData.value);
-  };
-  // 初始化时间范围
-  const timeRangeFormat = () => {
-    // 处理时间范围:将[8,0]转换成"08:00"
-    console.log('🚀 ~ timeRangeFormat ~ props:', props.reportData.startTime);
-    if (props.reportData.startTime) {
-      const startArr = props.reportData.startTime;
-      formData.value.startTime = `${startArr[0].toString().padStart(2, '0')}:${startArr[1]
-        .toString()
-        .padStart(2, '0')}`;
-    }
-    if (props.reportData.endTime) {
-      const endArr = props.reportData.endTime;
-      formData.value.endTime = `${endArr[0].toString().padStart(2, '0')}:${endArr[1].toString().padStart(2, '0')}`;
-    }
-  };
-  // 处理施工设备及未选择设备名称
-  const handleEquipmentNames = deviceIds => {
-    console.log('🚀 ~ handleEquipmentNames ~ deviceIds:', deviceIds);
-    formData.value.deviceIds = deviceIds || []; //施工设备
-    console.log('🚀 ~ handleEquipmentNames ~ formData.value.deviceIds:', formData.value.deviceIds);
-    const { selectedDevices = [] } = props.reportData || {};
-    const deviceIdSet = new Set(deviceIds);
-    //   已选择的设备(名称)
-    selectedEquipmentNames.value = selectedDevices
-      .filter(item => deviceIdSet.has(item.id))
-      .map(item => item.deviceName)
-      .join(',');
-
-    //   未选择的设备(名称)
-    unselectedEquipmentNames.value =
-      selectedDevices
-        .filter(item => !deviceIdSet.has(item.id))
-        .map(item => item.deviceName)
-        .join(',') || t('ruiDu.allEquipmentConstructed');
-  };
-  // 点击施工设备选择器
-  const handleClickSelectDevice = () => {
-    deviceTransferRef.value.open();
-  };
-  // 设备选择器回调
-  const handleTransferChange = selectedIds => {
-    console.log('🚀 ~ handleTransferChange ~ selectedIds:', selectedIds);
-    // 更新已选择的设备及名称
-    handleEquipmentNames(selectedIds);
-  };
-  // 移除已选择的施工工艺
-  const removeSelectedItem = value => {
-    console.log('🚀 ~ removeSelectedItem ~ value:', value);
-    formData.value.techniqueIds = formData.value.techniqueIds.filter(item => item !== value);
-  };
-
-  // 根据已选择的施工工艺对应的工作量属性字段
-  const getWorkloadInfoByTechnique = () => {
-    getRuiDuReportAttrs({
-      techniqueIds: formData.value.techniqueIds.join(','),
-    }).then(res => {
-      console.log('🚀 ~ getWorkloadInfoByTechnique ~ res:', res);
-      const { data = [] } = res;
-
-      // 1. 按 "identifier+unit" 去重:用Map保证唯一,key为拼接字段
-      const uniqueMap = new Map();
-      data.forEach(item => {
-        // 生成去重key(identifier和unit都存在才拼接,避免异常)
-        const key =
-          item.identifier && item.unit ? `${item.identifier}-${item.unit}` : Math.random().toString(36).slice(2, 11); // 异常情况用随机key避免重复
-        uniqueMap.set(key, item); // 重复key会覆盖,实现去重
-      });
-      // 去重后的数组
-      const uniqueData = Array.from(uniqueMap.values());
-
-      // 2. 对比formData.extProperty,保留已有actualValue(避免覆盖用户输入)
-      const handledData = uniqueData.map(newItem => {
-        // 生成当前新项的去重key
-        const newKey = newItem.identifier && newItem.unit ? `${newItem.identifier}-${newItem.unit}` : '';
-
-        // 在原有extProperty中找匹配项
-        const oldItem = formData.value.extProperty.find(old => {
-          const oldKey = old.identifier && old.unit ? `${old.identifier}-${old.unit}` : '';
-          return newKey && oldKey && newKey === oldKey;
-        });
-
-        // 有匹配项则复用原有actualValue,无则用新项的(默认空)
-        return {
-          ...newItem,
-          actualValue: oldItem?.actualValue ?? newItem.actualValue,
-        };
-      });
-
-      // 3. 重新赋值给formData.extProperty(更新页面展示)
-      formData.value.extProperty = handledData;
-
-      console.log('formData.value.extProperty :>> ', formData.value.extProperty);
-    });
-  };
-  // 上传附件(超过50M提示并删除文件列表展示)
-  const uploadFiles = async event => {
-    console.log('🚀 ~ uploadFiles ~ event:', event);
-    const tempFiles = event.tempFiles || []; // 选择的临时文件列表
-    const maxSize = 50 * 1024 * 1024; // 50M(字节)
-    const overSizeFiles = []; // 存储超过50M的文件
-
-    // 1. 筛选超过50M的文件,记录文件名
-    tempFiles.forEach(file => {
-      if (file.size > maxSize) {
-        overSizeFiles.push(file.name);
-      }
-    });
-
-    // 2. 有超过50M的文件:提示+从列表中移除
-    if (overSizeFiles.length > 0) {
-      // 提示信息
-      uni.showToast({
-        title: `【${overSizeFiles.join('、')}】${t('ruiDu.fileSizeLimit')}`,
-        icon: 'none',
-        duration: 3000,
-      });
-
-      // 关键:从formData.attachments中移除超过50M的文件(根据文件名匹配)
-      formData.value.attachments = formData.value.attachments.filter(attach => !overSizeFiles.includes(attach.name));
-
-      // 兼容:强制更新组件状态(避免uni-file-picker仍展示已移除文件)
-      await nextTick();
-      return; // 终止后续上传逻辑
-    }
-
-    // 3. 所有文件大小合法,执行正常上传逻辑(原有代码保留)
-    try {
-      for (const file of tempFiles) {
-        const uploadTask = await uploadAttachmentsFile(file.path, undefined);
-        console.log('🚀 ~ uploadFiles ~ uploadTask:', uploadTask);
-        if (uploadTask.code === 0) {
-          console.log('上传成功:', uploadTask);
-          const { data } = uploadTask;
-          // 根据返回结果更新附件信息
-          formData.value.attachments.push({
-            bizId: props.reportId,
-            category: 'daily_report',
-            filePath: data.files[0].filePath,
-            filename: data.files[0].name,
-            // fileSize: data.files[0].size,
-            // fileType: data.files[0].type,
-            remark: '',
-            type: 'attachment',
-          });
-          // 更新展示用的文件列表
-          attachmentsFileList.value.push({
-            ...data.files[0],
-            name: data.files[0].name,
-            url: data.files[0].filePath,
-          });
-        } else {
-          console.error('上传失败:', uploadTask);
-          uni.showToast({
-            title: `【${file.name}】${t('operation.uploadFail')}`,
-            icon: 'none',
-          });
-        }
-      }
-    } catch (err) {
-      console.error('上传异常:', err);
-      uni.showToast({ title: '上传异常,请重试', icon: 'none' });
-    }
-  };
-  // 删除附件
-  const deleteFiles = event => {
-    console.log('🚀 ~ deleteFiles ~ event:', event);
-    const { tempFile, index } = event;
-    // 1. 从formData.attachments中移除选中项
-    formData.value.attachments.splice(index, 1);
-    // 2. 从展示用的文件列表中移除选中项
-    attachmentsFileList.value.splice(index, 1);
-  };
-  // 下载文件
-  const downloadFile = async file => {
-    console.log('🚀 ~ downloadFile ~ file:', file);
-    const { url: fileUrl, name: fileName } = file;
-    if (!fileUrl) {
-      uni.showToast({ title: t('operation.fileUrlEmpty'), icon: 'none' });
-      return;
-    }
-    // 获取平台
-    const platform = uni.getSystemInfoSync().platform;
-    console.log('🚀 ~ downloadFile ~ platform:', platform);
-    // 判断平台
-    if (platform === 'android') {
-      uni.downloadFile({
-        url: fileUrl,
-        success: res => {
-          console.log('🚀 ~ downloadFile ~ res:', res);
-          if (res.statusCode === 200) {
-            uni.saveFile({
-              tempFilePath: res.tempFilePath,
-              success: res => {
-                console.log('🚀 ~ downloadFile saveFile ~ res:', res);
-                uni.showToast({
-                  title: t('operation.downloadSuccess'),
-                  icon: 'none',
-                });
-              },
-              fail: err => {
-                console.log('🚀 ~ downloadFile saveFile ~ err:', err);
-                uni.showToast({
-                  title: t('operation.downloadFail'),
-                  icon: 'none',
-                });
-              },
-            });
-          }
-        },
-        fail: err => {
-          console.log('🚀 ~ downloadFile ~ err:', err);
-          uni.showToast({
-            title: t('operation.downloadFail'),
-            icon: 'none',
-          });
-        },
-      });
-    } else {
-      try {
-        // 2.1 处理文件名(避免特殊字符乱码,如中文、空格)
-        const safeFileName = decodeURIComponent(fileName || '未命名文件'); // 解码URL编码的文件名
-
-        // 2.2 创建隐藏的 <a> 标签(核心:利用 download 属性触发下载)
-        const link = document.createElement('a');
-        // 关键:设置 download 属性指定文件名(Web 端独有)
-        link.download = safeFileName;
-        // 设置文件地址(若跨域,需后端配置 CORS + Content-Disposition 响应头)
-        link.href = fileUrl;
-        // 隐藏 <a> 标签(不影响页面布局)
-        link.style.display = 'none';
-        // 将 <a> 标签添加到文档中(否则部分浏览器无法触发点击)
-        document.body.appendChild(link);
-
-        // 2.3 模拟点击 <a> 标签触发下载
-        link.click();
-
-        // 2.4 下载后清理资源(避免内存泄漏)
-        setTimeout(() => {
-          document.body.removeChild(link); // 移除 <a> 标签
-          URL.revokeObjectURL(link.href); // 释放 URL 资源(若使用 Blob 时必需)
-        }, 100);
-      } catch (err) {
-        console.error('🚀 ~ Web 端下载失败:', err);
-        // 若直接通过 <a> 标签下载失败(如跨域),尝试通过 Blob 流下载(场景2兼容)
-        await downloadFileByBlob(fileUrl, fileName);
-      }
-    }
-  };
-  // 兼容场景2:通过 Blob 流下载(解决跨域或后端返回流的情况)
-  const downloadFileByBlob = async (fileUrl, fileName) => {
-    try {
-      // 1. 发起请求获取文件流(注意:需设置 responseType: 'blob')
-      const response = await fetch(fileUrl, {
-        method: 'GET',
-        headers: {
-          // 若需要登录态,添加 token(根据项目授权方式调整)
-          Authorization: `Bearer ${uni.getStorageSync('token')}`,
-        },
-      });
-
-      if (!response.ok) {
-        throw new Error(`请求失败: ${response.status}`);
-      }
-
-      // 2. 将响应转换为 Blob 对象(根据文件类型设置 MIME,如 PDF 为 'application/pdf')
-      const blob = await response.blob();
-      // 3. 生成 Blob 临时 URL
-      const blobUrl = URL.createObjectURL(blob);
-
-      // 4. 用 <a> 标签触发下载(同场景1逻辑)
-      const safeFileName = decodeURIComponent(fileName || '未命名文件');
-      const link = document.createElement('a');
-      link.download = safeFileName;
-      link.href = blobUrl;
-      link.style.display = 'none';
-      document.body.appendChild(link);
-      link.click();
-
-      // 5. 清理资源
-      setTimeout(() => {
-        document.body.removeChild(link);
-        URL.revokeObjectURL(blobUrl); // 必须释放 Blob URL,避免内存泄漏
-        uni.hideLoading();
-        // uni.showToast({ title: t("operation.downloadSuccess"), icon: "success" });
-      }, 100);
-    } catch (err) {
-      console.error('🚀 ~ Blob 下载失败:', err);
-      uni.hideLoading();
-      // uni.showToast({ title: t("operation.downloadFail"), icon: "error" });
-    }
-  };
-  // -------------------------- 监听 --------------------------
-  // 监听reportData变化
-  watch(
-    () => props.reportData,
-    (newVal, oldVal) => {
-      console.log('监听reportData变化 ~ newVal:', newVal);
-      console.log('🚀监听reportData变化 ~ oldVal:', oldVal);
-      if (newVal?.id) {
-        // 初始化表单数据
-        formDataFormat();
-      }
-    },
-    { deep: true, immediate: true }
-  );
-  // 监听施工工艺变化
-  watch(
-    () => formData.value.techniqueIds,
-    (newVal, oldVal) => {
-      console.log('监听施工工艺变化 ~ newVal:', newVal);
-      console.log('🚀监听施工工艺变化 ~ oldVal:', oldVal);
-      if (newVal.length) {
-        // 根据已选择的施工工艺对应的工作量属性字段
-        getWorkloadInfoByTechnique();
-      } else {
-        // 清空工作量属性字段
-        formData.value.extProperty = [];
-      }
-    },
-    { deep: true, immediate: true }
-  );
-  // -------------------------- 暴露给父组件的外部方法 --------------------------
-  defineExpose({
-    submitForm,
-  });
-  // -------------------------- 事件派发 --------------------------
-  const emit = defineEmits([]);
-</script>
-
-<style lang="scss" scoped>
-  @import '@/style/work-order-form.scss';
-  .report-form {
-    height: 100%;
-    color: #333;
-  }
-  :deep(.uni-textarea-textarea:disabled),
-  :deep(.uni-input-input:disabled) {
-    color: #333;
-  }
-  :deep(.uni-date-x) {
-    color: #333;
-  }
-  .digit-item {
-    text-align: right;
-    :deep(.uni-easyinput__content-input) {
-      padding-right: 10px;
-    }
-  }
-  .time-range-item {
-    margin: 10px;
-  }
-  .form-item-btn {
-    margin: 10px 0 0 0;
-    width: 60px;
-    text-align: center;
-  }
-  .form-item-select {
-    width: 100%;
-  }
-  .slot-box {
-    width: 100%;
-    flex-wrap: wrap;
-  }
-  .slot-content-item {
-    &.selected {
-      background: #f4f4f5;
-      // color: #909399;
-      margin: 5px;
-      padding: 5px;
-      border-radius: 3px;
-    }
-  }
-  .slot-item-text {
-    width: 90%;
-  }
-  .file-picker-container {
-    width: 100%;
-  }
-  .file-picker-btn {
-    margin-left: unset;
-    margin-right: unset;
-  }
-  .file-size-limit {
-    font-size: 10px;
-    color: #ff4500;
-    padding: 0 10px;
-  }
-  .file-list {
-    position: relative;
-    width: 100%;
-    .file-item {
-      position: relative;
-      width: 100%;
-      box-sizing: border-box;
-      padding: 8px 5px;
-      margin-bottom: 10px;
-      border: 1px solid #f5f5f5;
-      // 关键1:允许flex容器换行,适应文字高度
-      flex-wrap: wrap;
-      // 关键2:设置对齐方式,避免换行后按钮错位
-      align-items: flex-start;
-      .file-name {
-        margin-right: 8px;
-        // 关键3:允许文字换行
-        white-space: normal;
-        // 关键4:设置换行规则(中文按字换行,英文按词换行)
-        word-wrap: break-word;
-        word-break: break-all;
-      }
-      .file-btn {
-        color: #004098;
-        min-width: max-content;
-        // 关键5:按钮垂直居中(即使文字换行,按钮也居中)
-        align-self: center;
-      }
-    }
-  }
-</style>

+ 155 - 122
pages/ruiDu/compontents/report-form.vue

@@ -1,20 +1,20 @@
 <script setup>
   import { useDataDictStore } from '@/store/modules/dataDict';
-  import { storeToRefs } from 'pinia';
-  import { onMounted, reactive, ref, computed, getCurrentInstance, watch, nextTick } from 'vue';
+  import { onMounted, reactive, ref, computed, getCurrentInstance, watch } from 'vue';
 
   import tpfTimeRange from '@/components/tpf-time-range/tpf-time-range.vue';
   import deviceTransfer from '@/components/device-transfer/index.vue';
 
-  import { getRuiDuReportAttrs, uploadAttachmentsFile, updateRuiDuReportBatch } from '@/api/ruiDu.js';
+  import { updateRuiDuReportBatch, getRuiDuReportAttrs } from '@/api/ruiDu.js';
+  import config from '@/utils/config';
+
+  import { getTenantId, getAccessToken } from '@/utils/auth.js';
 
   const { appContext } = getCurrentInstance();
   const t = appContext.config.globalProperties.$t;
 
   const { getIntDictOptions, getStrDictOptions, loadDataDictList } = useDataDictStore();
 
-  const { dataDict } = storeToRefs(useDataDictStore());
-
   const rdStatusRange = ref([]);
   const techniqueRange = ref([]);
 
@@ -221,8 +221,6 @@
     handleEquipmentNames(selectedIds);
   };
 
-  const attachmentsFileList = ref([]);
-
   const formDataFormat = () => {
     // 处理时间范围
     timeRangeFormat();
@@ -234,7 +232,11 @@
 
     console.log('props.reportData :>> ', props.reportData);
 
-    form.platformIds = [props.reportData.id];
+    if (props.formDisable) {
+      form.platformIds = props.reportData.platforms?.map(v => v.reportId) ?? [];
+    } else {
+      form.platformIds = [props.reportData.id];
+    }
 
     if (props.reportData.platformWell === 1) {
       props.reportData.platforms.forEach(p => {
@@ -304,86 +306,130 @@
     timeRangeRef.value.open();
   };
 
-  const uploadFiles = async event => {
-    const tempFiles = event.tempFiles || []; // 选择的临时文件列表
-    const maxSize = 50 * 1024 * 1024; // 50M(字节)
-    const overSizeFiles = []; // 存储超过50M的文件
-
-    // 1. 筛选超过50M的文件,记录文件名
-    tempFiles.forEach(file => {
-      if (file.size > maxSize) {
-        overSizeFiles.push(file.name);
-      }
+  const uploadRef = ref(null);
+
+  const attachmentList = ref([]);
+
+  const chooseFile = () => {
+    uploadRef.value.chooseFile({
+      count: 9,
+      size: 50,
+      success: files => {
+        attachmentList.value = attachmentList.value.concat(files);
+        attachmentList.value.forEach(file => {
+          // 将等待上传和上传失败的文件提交上传到服务器
+          // 提示:::如果接口不支持跨域,改成调用this.getTempFilePath(file)
+          if (file.status === 'waiting' || file.status === 'fail') {
+            uploadHandle(file);
+          }
+        });
+      },
     });
+  };
 
-    // 2. 有超过50M的文件:提示+从列表中移除
-    if (overSizeFiles.length > 0) {
-      // 提示信息
-      uni.showToast({
-        title: `【${overSizeFiles.join('、')}】${t('ruiDu.fileSizeLimit')}`,
-        icon: 'none',
-        duration: 3000,
-      });
-
-      // 关键:从formData.attachments中移除超过50M的文件(根据文件名匹配)
-      form.attachments = form.attachments.filter(attach => !overSizeFiles.includes(attach.name));
-
-      // 兼容:强制更新组件状态(避免uni-file-picker仍展示已移除文件)
-      await nextTick();
-      return; // 终止后续上传逻辑
-    }
-
-    // 3. 所有文件大小合法,执行正常上传逻辑(原有代码保留)
-    try {
-      for (const file of tempFiles) {
-        const uploadTask = await uploadAttachmentsFile(file.path, undefined);
-        console.log('🚀 ~ uploadFiles ~ uploadTask:', uploadTask);
-        if (uploadTask.code === 0) {
-          console.log('上传成功:', uploadTask);
-          const { data } = uploadTask;
-          // 根据返回结果更新附件信息
-          form.attachments.push({
-            bizId: props.reportId,
-            category: 'daily_report',
-            filePath: data.files[0].filePath,
-            filename: data.files[0].name,
-            // fileSize: data.files[0].size,
-            // fileType: data.files[0].type,
-            remark: '',
-            type: 'attachment',
-          });
-          // 更新展示用的文件列表
-          attachmentsFileList.value.push({
-            ...data.files[0],
-            name: data.files[0].name,
-            url: data.files[0].filePath,
-          });
-        } else {
-          console.error('上传失败:', uploadTask);
+  const uploadHandle = file => {
+    uploadRef.value.upload({
+      url: config.default.apiUrl + config.default.apiUrlSuffix + '/rq/file/upload',
+      file,
+      name: 'files',
+      header: {
+        'Authorization': getAccessToken() ? 'Bearer ' + getAccessToken() : '',
+        'Tenant-id': getTenantId() ? getTenantId() : '1',
+        'Device-id': 'undefined',
+      },
+      method: 'post',
+      success: e => {
+        file.status = 'success';
+        const result = JSON.parse(e.result);
+
+        if (result.code !== 0) {
           uni.showToast({
             title: `【${file.name}】${t('operation.uploadFail')}`,
             icon: 'none',
           });
+          return;
         }
-      }
-    } catch (err) {
-      console.error('上传异常:', err);
-      uni.showToast({ title: '上传异常,请重试', icon: 'none' });
-    }
+
+        console.log('11 :>> ', 11);
+
+        form.attachments.push({
+          bizId: props.reportId,
+          category: 'daily_report',
+          filePath: result.data.files[0].filePath,
+          filename: result.data.files[0].name,
+          // fileSize: data.files[0].size,
+          // fileType: data.files[0].type,
+          remark: '',
+          type: 'attachment',
+        });
+
+        console.log('form.attachments :>> ', form.attachments);
+      },
+      fail: e => {
+        file.status = 'fail';
+        console.error('上传异常:', err);
+        uni.showToast({ title: t('operation.uploadFail'), icon: 'none' });
+      },
+    });
   };
+
+  // const uploadHandle = file => {
+  //   uploadRef.value.getTempFilePath({
+  //     file,
+  //     success: e => {
+  //       uni.uploadFile({
+  //         url: config.default.apiUrl + config.default.apiUrlSuffix + '/rq/file/upload',
+  //         header: {
+  //           'Authorization': getAccessToken() ? 'Bearer ' + getAccessToken() : '',
+  //           'tenant-id': getTenantId() ? getTenantId() : '1',
+  //           'device-id': undefined,
+  //         },
+  //         name: 'files',
+  //         // #ifdef H5
+  //         file: e.result,
+  //         // #endif
+  //         // #ifndef H5
+  //         filePath: e.result,
+  //         // #endif
+  //         success: e => {
+  //           console.log('e :>> ', e);
+  //           //             file.status = 'success';
+  //           // const result = JSON.parse(e.result);
+  //           // if (e.code !== 0) {
+  //           //   uni.showToast({
+  //           //     title: `【${file.name}】${t('operation.uploadFail')}`,
+  //           //     icon: 'none',
+  //           //   });
+  //           //   return;
+  //           // }
+  //           // form.attachments.push({
+  //           //   bizId: props.reportId,
+  //           //   category: 'daily_report',
+  //           //   filePath: data.files[0].filePath,
+  //           //   filename: data.files[0].name,
+  //           //   // fileSize: data.files[0].size,
+  //           //   // fileType: data.files[0].type,
+  //           //   remark: '',
+  //           //   type: 'attachment',
+  //           // });
+  //         },
+  //         fail: e => {
+  //           console.log('e :>> ', e);
+  //         },
+  //       });
+  //     },
+  //   });
+  // };
+
   // 删除附件
-  const deleteFiles = event => {
-    console.log('🚀 ~ deleteFiles ~ event:', event);
-    const { tempFile, index } = event;
+  const deleteFiles = index => {
     // 1. 从formData.attachments中移除选中项
     form.attachments.splice(index, 1);
-    // 2. 从展示用的文件列表中移除选中项
-    attachmentsFileList.value.splice(index, 1);
   };
   // 下载文件
   const downloadFile = async file => {
     console.log('🚀 ~ downloadFile ~ file:', file);
-    const { url: fileUrl, name: fileName } = file;
+    const { filePath: fileUrl, name: fileName } = file;
     if (!fileUrl) {
       uni.showToast({ title: t('operation.fileUrlEmpty'), icon: 'none' });
       return;
@@ -697,35 +743,27 @@
         </uni-forms-item>
         <!-- 附件 -->
         <uni-forms-item class="form-item" :label="`${$t('ruiDu.attachment')}:`" :required="false" name="attachments">
-          <uni-file-picker
-            file-mediatype="all"
-            :clear="false"
-            :limit="9"
-            :auto-upload="false"
-            :readonly="formDisable"
-            v-model="attachmentsFileList"
-            @select="uploadFiles"
-            @delete="deleteFiles"
-            v-if="!formDisable">
+          <view v-if="!formDisable" class="file-parent">
             <view class="file-picker-container item-end">
               <view class="file-size-limit">
                 {{ $t('ruiDu.fileSizeLimit') }}
               </view>
-              <button type="primary" size="mini" class="file-picker-btn">
+              <button type="primary" size="mini" class="file-picker-btn" @click="chooseFile">
                 {{ $t('ruiDu.selectFile') }}
               </button>
             </view>
-          </uni-file-picker>
+            <view class="file-list">
+              <view v-for="(file, index) in form.attachments" :key="index">
+                {{ file.filename }}
+                <button @click="deleteFiles" type="primary" size="mini" class="file-picker-btn">删除文件</button>
+              </view>
+            </view>
+          </view>
           <view class="file-list item-col" v-else>
-            <uni-row
-              class="file-item flex flex-row items-center justify-between"
-              v-for="(file, index) in attachmentsFileList"
-              :key="index">
-              <uni-col :span="20" class="file-name flex flex-row">{{ file.name }}</uni-col>
-              <uni-col :span="3" class="file-btn flex items-center" @click="downloadFile(file)">
-                {{ $t('operation.download') }}
-              </uni-col>
-            </uni-row>
+            <view v-for="(file, index) in form.attachments" :key="index">
+              {{ file.filename }}
+              <button @click="downloadFile(file)" type="primary" size="mini" class="file-picker-btn">下载文件</button>
+            </view>
           </view>
         </uni-forms-item>
       </div>
@@ -847,6 +885,8 @@
     </uni-forms>
   </scroll-view>
 
+  <lsj-upload ref="uploadRef"></lsj-upload>
+
   <tpf-time-range
     ref="timeRangeRef"
     :startTime="startTime"
@@ -913,40 +953,34 @@
     margin-left: unset;
     margin-right: unset;
   }
+
   .file-size-limit {
     font-size: 10px;
     color: #ff4500;
     padding: 0 10px;
   }
 
+  .file-parent {
+    display: flex;
+    flex-direction: column;
+    align-items: end;
+    padding: 10px 0;
+  }
+
   .file-list {
-    position: relative;
-    width: 100%;
-    .file-item {
-      position: relative;
+    display: flex;
+    flex-direction: column;
+    align-items: end;
+    justify-content: end;
+    width: 300px;
+
+    & > view {
       width: 100%;
-      box-sizing: border-box;
-      padding: 8px 5px;
-      margin-bottom: 10px;
-      border: 1px solid #f5f5f5;
-      // 关键1:允许flex容器换行,适应文字高度
-      flex-wrap: wrap;
-      // 关键2:设置对齐方式,避免换行后按钮错位
-      align-items: flex-start;
-      .file-name {
-        margin-right: 8px;
-        // 关键3:允许文字换行
-        white-space: normal;
-        // 关键4:设置换行规则(中文按字换行,英文按词换行)
-        word-wrap: break-word;
-        word-break: break-all;
-      }
-      .file-btn {
-        color: #004098;
-        min-width: max-content;
-        // 关键5:按钮垂直居中(即使文字换行,按钮也居中)
-        align-self: center;
-      }
+      padding: 10px;
+      padding-right: 0;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
     }
   }
 
@@ -996,8 +1030,7 @@
   }
 
   .item-col {
-    display: flex;
-    flex-direction: column;
+    float: right;
   }
 
   .time-range-item {

+ 160 - 0
uni_modules/lsj-upload/changelog.md

@@ -0,0 +1,160 @@
+## 3.0.0(2025-07-17)
+【重构】重构为API式调用
+## 2.6.2(2024-12-06)
+【修复】检查到cli创建的 Vue3项目@change事件与Vue冲突导致额外触发一次且files传入为null的问题,现将@change回调更名为@changeFile
+## 2.5.1(2024-11-27)
+删除默认的录音权限校验请求,若需要录音权限或其他权限校验可通过props.permission设置
+## 2.4.9(2024-09-02)
+优化:回调函数增加返回childId字段
+## 2.4.8(2024-08-31)
+修复小程序编译报错问题
+## 2.4.7(2024-08-31)
+修复小程序编译报错问题
+## 2.4.6(2024-08-06)
+部分设备兼容问题处理
+## 2.4.5(2024-08-01)
+修复重复文件重命名时跳过了文件(1)直接从(2)开始的问题
+## 2.4.4(2024-07-31)
+【问题修复】修复不去重文件时重名文件只能选择2个的问题
+## 2.4.3(2024-07-29)
+问题修复
+## 2.4.2(2024-07-17)
+已知问题修复
+## 2.4.1(2024-07-17)
+已知问题修复
+## 2.4.0(2024-07-16)
+【老版本Android系统兼容问题修复】修复2.3.7添加内置权限校验后部分老安卓系统不兼容input.click时未能正常弹出文件选择界面的问题,处理方式-- 检测到若不兼容input.click则使用默认input file,此时若需要检测权限请自己在页面添加(当然,上华为商店审核时应该不存在老安卓系统问题)
+## 2.3.9(2024-07-16)
+移除测试代码(老是忘记,汗!!)
+## 2.3.8(2024-07-16)
+【优化】组件增加属性:toBase 文件是否转base64,使用和注意事项查看下方js示例注释说明。
+## 2.3.7(2024-07-04)
+已知问题修复
+## 2.3.6(2024-07-04)
+已知问题修复
+## 2.3.5(2024-07-03)
+【优化】优化权限校验,增加权限校验时顶部弹框
+## 2.3.4(2024-06-27)
+【兼容优化】修复部分老安卓系统设备设置不去重时不兼容问题
+## 2.3.3(2024-06-26)
+【新增】APP端内置权限校验。目前仅在OPPO FINDX机型测试验证,若其他机型有问题可加群反馈,或找我拿v2.3.2稳定版源码。
+## 2.3.2(2024-06-13)
+问题修复:2.3.1版本引起的部分设备不支持findLastIndex问题
+## 2.3.1(2024-05-20)
+修复:文件不去重时返回文件列表name与组件内置列表key不一致问题。
+## 2.3.0(2024-05-20)
+优化:1:增加属性distinct【选择文件是否去重】、2:对show/hide函数增加uni.$emit事件监听,若页面存在多个上传组件时,可通过uni.$emit控制所有上传组件webview透明层是否显示。
+## 2.2.9(2023-06-01)
+优化:将是否多选与count字段解绑(原逻辑是count>1为允许多选),改为新增multiple属性控制是否多选。
+## 2.2.8(2023-06-01)
+修复上版本提交时accept测试值未删除导致h5端只能选择图片的问题。
+## 2.2.7(2023-05-06)
+应群友建议,当instantly为true时,触发change事件后延迟1000毫秒再自动上传,方便动态修改参数,其实个人还是建议想在change事件动态设置参数的伙伴将instantly设置为false,修改参数后手动调用upload()
+## 2.2.6(2023-02-09)
+修复多个文件同时选择时返回多次change回调的问题
+## 2.2.5(2022-12-27)
+1.修复多选文件时未能正常校验数量的问题;
+2.app端与H5端支持单选或多选文件,通过count数量控制,超过1开启多选。
+## 2.2.4(2022-12-27)
+1.修复多选文件时未能正常校验数量的问题;
+2.app端修复多选只取到第一个文件的问题。
+## 2.2.3(2022-12-06)
+修复手动调用show()导致count失效的问题
+## 2.2.2(2022-12-01)
+Vue3自行修改兼容
+## 2.2.1(2022-10-19)
+修复childId警告提示
+## 2.2.0(2022-10-10)
+更新app端webview窗口参数clidId,默认值添加时间戳保证唯一性
+## 2.1.9(2022-07-13)
+[修复] app端选择文件后初始化设置的文件列表被清空问题
+## 2.1.8(2022-07-13)
+[新增] ref方法初始化文件列表,用于已提交后再次编辑时需带入已上传文件:setFiles(files),可传入数组或Map对象,传入格式请与组件选择返回格式保持一致,且name为必须属性。
+## 2.1.7(2022-07-12)
+修复ios端偶现创建webview初始化参数未生效的问题
+## 2.1.6(2022-07-11)
+[修复]:修复上个版本更新导致nvue窗口组件不能选择文件的问题;
+[新增]:
+1.应群友建议(填写禁止格式太多)格式限制formats由原来填写禁止选择的格式改为填写允许被选择的格式;
+2.应群友建议(增加上传结束回调事件),上传结束回调事件@uploadEnd
+3.如能帮到你请留下你的免费好评,组件使用过程中有问题可以加QQ群交流,至于Map对象怎么使用这类前端基础问题请自行百度
+## 2.1.5(2022-07-01)
+app端组件销毁时添加自动销毁webview功能,避免v-if销毁组件的情况控件还能被点击的问题
+## 2.1.4(2022-07-01)
+修复小程序端回显问题
+## 2.1.3(2022-06-30)
+回调事件返回参数新增path字段(文件临时地址),用于回显
+## 2.1.2(2022-06-16)
+修复APP端Tabbar窗口无法选择文件的问题
+## 2.1.1(2022-06-16)
+优化:
+1.组件优化为允许在v-if中使用;
+2.允许option直接在data赋值,不再强制在onRead中初始化;
+## 2.1.0(2022-06-13)
+h5 pc端更改为单次可多选
+## 2.0.9(2022-06-10)
+更新演示内容,部分同学不知道怎么获取服务端返回的数据
+## 2.0.8(2022-06-09)
+优化动态更新上传参数函数,具体查看下方说明:动态更新参数演示
+## 2.0.7(2022-06-07)
+新增wxFileType属性,用于小程序端选择附件时可选文件类型
+## 2.0.6(2022-06-07)
+修复小程序端真机选择文件提示失败的问题
+## 2.0.5(2022-06-02)
+优化小程序端调用hide()后未阻止触发文件选择问题
+## 2.0.4(2022-06-01)
+优化APP端选择器初始定位
+## 2.0.3(2022-05-31)
+修复nvue窗口选择文件报错问题 
+## 2.0.2(2022-05-20)
+修复ios端opiton设置过早未传入webview导致不自动上传问题
+## 2.0.1(2022-05-19)
+修复APP端子窗口点击选择文件不响应问题
+## 2.0.0(2022-05-18)
+此次组件更新至2.0版本,与1.0版本使用上略有差异,已使用1.0的同学请自行斟酌是否需要升级!
+部分差异:
+一、 2.0新增异步触发上传功能;
+二、2.0新增文件批量上传功能;
+三、2.0优化option,剔除属性,只保留上传接口所需字段,且允许异步更改option的值;
+四、组件增加size(文件大小限制)、count(文件个数限制)、formats(文件后缀限制)、accept(文件类型限制)、instantly(是否立即自动上传)、debug(日志打印)等属性;
+五、回调事件取消input事件、callback事件,新增change事件和progress事件;
+六、ref事件新增upload事件、clear事件;
+七、优化组件代码,show和hide函数改为显示隐藏,不再重复开关webview;
+
+## 1.2.3(2022-03-22)
+修复Demo里传入待完善功能[手动上传属性manual=true]导致不自动上传的问题,手动提交上传待下个版本更新
+## 1.2.2(2022-02-21)
+修复上版本APP优化导致H5和小程序端不自动初始化的问题,此次更新仅修复此问题。异步提交功能下个版本更新~
+## 1.2.1(2022-01-25)
+QQ1群已满,已开放2群:469580165
+## 1.2.0(2021-12-09)
+优化APP端页面中DOM重排后每次需要重新定位的问题
+## 1.1.1(2021-12-09)
+优化,与上版本使用方式有改变,请检查后确认是否需要更新,create更名为show,  close更名为hide,取消初始化时手动create, 传参方式改为props=>option
+## 1.1.0(2021-12-09)
+新增refresh方法,用于DOM发生重排时重新定位控件(APP端)
+## 1.0.9(2021-07-15)
+修复上传进度未同步渲染,直接返回100%的BUG
+## 1.0.8(2021-07-12)
+修复H5端传入height和width未生效的bug
+## 1.0.7(2021-07-07)
+修复h5和小程序端上传完成callback未返回fileName字段问题
+## 1.0.6(2021-07-07)
+修复h5端提示信息debug
+## 1.0.5(2021-06-29)
+感谢小伙伴找出bug,上传成功回调success未置为true,已修复
+## 1.0.4(2021-06-28)
+新增兼容APP,H5,小程序手动关闭控件,关闭后不再弹出文件选择框,需要重新create再次开启
+## 1.0.3(2021-06-28)
+close增加条件编译,除app端外不需要close
+## 1.0.2(2021-06-28)
+1.修复页面滚动位置后再create控件导致控件位置不正确的问题;
+2.修复nvue无法create控件;
+3.示例项目新增nvue使用案例;
+## 1.0.1(2021-06-28)
+因为有的朋友不清楚app端切换tab时应该怎么处理webview,现重新上传一版示例项目,需要做tab切换的朋友可以导入示例项目查看
+## 1.0.0(2021-06-25)
+此插件为l-file插件中上传功能改版,更新内容为:
+1. 按钮内嵌入页面,不再强制固定底部,可跟随页面滚动
+2.无需再单独弹框点击上传,减去中间层
+3.通过slot自定义按钮样式

+ 1071 - 0
uni_modules/lsj-upload/components/lsj-upload/lsj-upload.vue

@@ -0,0 +1,1071 @@
+<template>
+  <view class="lsj-upload">
+    <!-- #ifndef MP -->
+    <view
+      id="render"
+      ref="render"
+      :config="config"
+      v-html="inputHtml"
+      :uploadParams="uploadParams"
+      :renderInput="renderInput"
+      :renderFile="renderFile"
+      :change:config="render.initConfig"
+      :change:renderFile="render.renderFileHandle"
+      :change:uploadParams="render.uploadHandle"
+      :change:renderInput="render.chooseFileHandle">
+    </view>
+    <!-- #endif -->
+  </view>
+</template>
+<script>
+  export default {
+    name: 'lsj-upload',
+    props: {
+      // 调试打印
+      debug: {
+        type: Boolean,
+        default: false,
+      },
+      // 是否去重文件(同名文件覆盖)
+      distinct: {
+        type: Boolean,
+        default: false,
+      },
+    },
+    data() {
+      return {
+        isIniting: false,
+        isUpdating: false,
+        isProcessing: false,
+        initQueue: [],
+        uploadQueue: [],
+        taskQueue: [],
+        chooseFileSuccess: null,
+        chooseFileFail: null,
+        uploadEventMap: new Map(),
+        baseEventMap: new Map(),
+        arrayBufferEventMap: new Map(),
+        tempFilePathEventMap: new Map(),
+        inputHtml: '',
+        inputId: '',
+        option: {},
+        uploadParams: '{}',
+        handleName: '',
+        handleType: '',
+        params: {},
+      };
+    },
+    computed: {
+      config() {
+        return {
+          debug: this.debug,
+          distinct: this.distinct,
+        };
+      },
+      renderInput() {
+        return {
+          id: this.inputId,
+          html: this.inputHtml,
+          option: this.option,
+        };
+      },
+      renderFile() {
+        return {
+          name: this.handleName,
+          type: this.handleType,
+          params: this.params,
+        };
+      },
+    },
+    methods: {
+      chooseFile(e) {
+        const { success, fail, ...option } = e;
+        if (success) {
+          this.chooseFileSuccess = success;
+        } else {
+          this.chooseFileSuccess = null;
+        }
+        if (fail) {
+          this.chooseFileFail = fail;
+        } else {
+          this.chooseFileFail = null;
+        }
+        this.option = option;
+
+        // #ifndef MP
+        this.inputId = +new Date();
+        this.inputHtml = `<input type="file" id="lsj-file-${this.inputId}" name="lsj-file" style="opacity:0" />`;
+        // #endif
+        // #ifdef MP
+        const { count = 9, type = 'all' } = option;
+        wx.chooseMessageFile({
+          count,
+          type,
+          success: ({ tempFiles }) => {
+            this.chooseFileSuccess(
+              tempFiles.map(_ => {
+                _['status'] = 'waiting';
+                _['progress'] = 0;
+                return _;
+              })
+            );
+          },
+          fail: () => {
+            this.toToast(`打开失败`);
+          },
+        });
+        // #endif
+      },
+      deleted(e) {
+        if (!e?.name) {
+          throw Error('abort ERROR: file is undefined');
+        }
+        this.resetHandle();
+        this.handleName = e.name;
+        this.handleType = 'deleted';
+      },
+      clear() {
+        this.resetHandle();
+        this.handleName = '';
+        this.handleType = 'clear';
+      },
+      abort(e) {
+        if (!e?.name) {
+          throw Error('abort ERROR: file is undefined');
+        }
+        // #ifndef MP
+        this.resetHandle();
+        this.handleName = e.name;
+        this.handleType = 'abort';
+        // #endif
+        // #ifdef MP
+        try {
+          this.uploadEventMap.set(params.file.name, {
+            xmlRequest,
+          });
+          const cuXmlRequest = this.uploadEventMap.get(params.file.name)?.xmlRequest;
+          if (cuXmlRequest) {
+            cuXmlRequest?.abort();
+          }
+        } catch (error) {
+          console.error(error);
+        }
+
+        // #endif
+      },
+      wxReadFile(params, encoding) {
+        uni.getFileSystemManager().readFile({
+          filePath: params?.file.path,
+          encoding,
+          success: res => {
+            if (params?.success && typeof params?.success === 'function') {
+              return params.success({
+                name: params.file.name,
+                result: res.data,
+              });
+            }
+          },
+          fail: params?.fail,
+        });
+      },
+      resetHandle() {
+        this.handleName = '';
+        this.handleType = '';
+      },
+      processInitQueue() {
+        if (this.isIniting || this.initQueue.length === 0) {
+          return;
+        }
+        this.isIniting = true;
+        const { handleName, handleType } = this.initQueue.shift();
+        setTimeout(() => {
+          this.resetHandle();
+          this.handleName = handleName;
+          this.handleType = handleType;
+          this.$nextTick(() => {
+            this.isIniting = false;
+            this.processInitQueue();
+          });
+        }, 300);
+      },
+      initHandle({ params, ob, handleType, types = ['success', 'fail', 'onprogress'] }) {
+        try {
+          if (!params?.file || !params?.file?.name) {
+            throw Error('ERROR: file is undefined');
+          }
+          const callFunctions = this.getCallBackFn(params, types);
+          ob.set(params.file.name, {
+            fileChunks: [], // 存储所有分片
+            totalChunks: 0, // 总分片数
+            receivedIndexes: new Set(), // 当前已接收的分片索引
+            ...callFunctions, // 回调函数
+          });
+
+          this.initQueue.push({
+            handleName: params.file?.name,
+            handleType,
+          });
+          if (!this.isIniting) {
+            this.processInitQueue();
+          }
+        } catch (error) {
+          console.error(error.message);
+        }
+      },
+      getCallBackFn(params, types) {
+        let o = {};
+        if (types.includes('success') && params?.success && typeof params?.success === 'function') {
+          o['success'] = params?.success;
+        }
+        if (types.includes('fail') && params?.fail && typeof params?.fail === 'function') {
+          o['fail'] = params?.fail;
+        }
+        if (types.includes('onprogress') && params?.onprogress && typeof params?.onprogress === 'function') {
+          o['onprogress'] = params?.onprogress;
+        }
+        return o;
+      },
+      addChunks(result, item) {
+        const { chunk, index, totalChunks } = result;
+        if (item.totalChunks === 0) {
+          item.totalChunks = totalChunks;
+        }
+        if (!item.receivedIndexes.has(index)) {
+          item.fileChunks[index] = chunk;
+          item.receivedIndexes.add(index);
+        }
+        return item;
+      },
+      executeEnd(e, maps) {
+        this.executeCallBackFn(e, maps, ['success', 'fail']);
+        this.baseEventMap.delete(e.name);
+        this.resetHandle();
+      },
+      executeCallBackFn(e, maps, types) {
+        const o = maps.get(e.name);
+        if (!o) {
+          return;
+        }
+        const [type] = types.filter(_ => e.status === _);
+        if (type && typeof o[type] === 'function') {
+          o[type](e);
+        }
+      },
+      getArrayBuffer(params) {
+        if (!params?.file || !params?.file?.name) {
+          throw Error('ERROR: file is undefined');
+        }
+        // #ifndef MP
+        this.initHandle({
+          params,
+          handleType: 'getArrayBuffer',
+          ob: this.arrayBufferEventMap,
+        });
+        // #endif
+        // #ifdef MP
+        this.wxReadFile(params);
+        // #endif
+      },
+      getArrayBufferCallBack(e) {
+        if (e.status === 'fail') {
+          return this.executeEnd(e, this.arrayBufferEventMap);
+        }
+        if (e.status === 'success') {
+          const item = this.addChunks(e.result, this.arrayBufferEventMap.get(e.name));
+          if (item.receivedIndexes.size === item.totalChunks) {
+            this.debug && console.log('分片结束');
+            const chunks = item.fileChunks.filter(chunk => chunk !== undefined);
+            const base64 = chunks.join('');
+            e.result = this.base64ToArrayBuffer(base64);
+            e.chunks = chunks;
+            return this.executeEnd(e, this.arrayBufferEventMap);
+          }
+        }
+      },
+      getBase(params) {
+        if (!params?.file || !params?.file?.name) {
+          throw Error('ERROR: file is undefined');
+        }
+        // #ifndef MP
+        this.initHandle({
+          params,
+          handleType: 'getBase',
+          ob: this.baseEventMap,
+        });
+        // #endif
+        // #ifdef MP
+        this.wxReadFile(params, 'base64');
+        // #endif
+      },
+      /**
+       * 将 base64 转换为 File 对象
+       * @param {String} base64Data - base64字符串,可以是带前缀的,如 data:image/png;base64,iVBORw...
+       * @param {String} fileName - 自定义文件名,比如 'image.png'
+       * @returns {File} 返回 File 对象
+       */
+      base64ToFile(base64Data, fileName, fileType) {
+        // 如果没有前缀,你可以手动传入 mime 或根据业务设置默认值
+        let mime = fileType || 'application/octet-stream'; // 默认二进制流,也可以根据情况设置如 'image/png'
+        let byteString = atob(base64Data);
+        let arrayBuffer = new ArrayBuffer(byteString.length);
+        let uint8Array = new Uint8Array(arrayBuffer);
+        for (let i = 0; i < byteString.length; i++) {
+          uint8Array[i] = byteString.charCodeAt(i);
+        }
+        let blob = new Blob([arrayBuffer], { type: mime });
+        let file = new File([blob], fileName, { type: mime });
+        return file;
+      },
+      base64ToArrayBuffer(base64) {
+        const binaryString = atob(base64);
+        const bytes = new Uint8Array(binaryString.length);
+        for (let i = 0; i < binaryString.length; i++) {
+          bytes[i] = binaryString.charCodeAt(i);
+        }
+        return bytes.buffer;
+      },
+      base64ToPath(base64, fileName) {
+        return new Promise((resolve, reject) => {
+          // #ifdef APP
+          const docDir = plus.io.convertLocalFileSystemURL('_doc/');
+          plus.io.resolveLocalFileSystemURL(
+            docDir,
+            dirEntry => {
+              dirEntry.getFile(
+                fileName,
+                {
+                  create: true,
+                },
+                fileEntry => {
+                  fileEntry.createWriter(
+                    writer => {
+                      writer.onwrite = e => {
+                        this.debug & console.log('文件写入成功:', `_doc/${fileName}`);
+                        resolve(`_doc/${fileName}`);
+                      };
+
+                      writer.onerror = e => {
+                        this.debug & console.error('文件写入失败:', e.toString());
+                        reject(e);
+                      };
+                      writer.writeAsBinary(base64);
+                    },
+                    error => {
+                      console.error('创建文件写入器失败:', error.toString());
+                      reject(error);
+                    }
+                  );
+                },
+                error => {
+                  console.error('获取文件条目失败:', error.toString());
+                  reject(error);
+                }
+              );
+            },
+            error => {
+              console.error('解析目录失败:', error.toString());
+              reject(error);
+            }
+          );
+          // #endif
+
+          // #ifndef APP
+          reject(new Error('该功能仅支持 App 端!'));
+          // #endif
+        });
+      },
+      getBaseCallBack(e) {
+        if (e.status === 'fail') {
+          return this.executeEnd(e, this.baseEventMap);
+        }
+        if (e.status === 'success') {
+          const item = this.addChunks(e.result, this.baseEventMap.get(e.name));
+          if (item.receivedIndexes.size === item.totalChunks) {
+            this.debug && console.log('分片结束');
+            const chunks = item.fileChunks.filter(chunk => chunk !== undefined);
+            e.result = chunks.join('');
+            e.chunks = chunks;
+            return this.executeEnd(e, this.baseEventMap);
+          }
+        }
+      },
+      processTaskQueue() {
+        if (this.isIniting) {
+          setTimeout(() => {
+            this.processTaskQueue();
+          }, 500);
+          return;
+        }
+        if (this.isProcessing || this.taskQueue.length === 0) {
+          return;
+        }
+        this.isProcessing = true;
+        const { handleName, handleType, params } = this.taskQueue.shift();
+        setTimeout(() => {
+          this.resetHandle();
+          this.handleName = handleName;
+          this.handleType = handleType;
+          this.params = params;
+          this.$nextTick(() => {
+            this.isProcessing = false;
+            this.processTaskQueue();
+          });
+        }, 200);
+      },
+      asyncFileTempFilePath(name, tempFilePath) {
+        this.taskQueue.push({
+          handleType: 'asyncFileTempFilePath',
+          handleName: name,
+          params: {
+            tempFilePath,
+          },
+        });
+        if (!this.isProcessing) {
+          this.processTaskQueue();
+        }
+      },
+      async getTempFilePathCallBack(e) {
+        if (e.status === 'fail') {
+          return this.executeEnd(e, this.tempFilePathEventMap);
+        }
+        try {
+          if (e.status === 'success') {
+            if (e?.result?.tempFilePath) {
+              e.result = e?.result?.tempFilePath;
+              return this.executeEnd(e, this.tempFilePathEventMap);
+            }
+            const item = this.addChunks(e.result, this.tempFilePathEventMap.get(e.name));
+            if (item.receivedIndexes.size === item.totalChunks) {
+              this.debug && console.log('分片结束');
+              const chunks = item.fileChunks.filter(chunk => chunk !== undefined);
+              const base64 = chunks.join('');
+              e.chunks = chunks;
+              // const arrayBuffer = this.base64ToArrayBuffer(base64);
+              // #ifdef H5
+              e.result = this.base64ToFile(base64, e.name, e.fileType);
+              // #endif
+              // #ifdef APP
+              e.result = await this.base64ToPath(base64, e.name);
+              this.asyncFileTempFilePath(e.name, e.result);
+              // #endif
+              return this.executeEnd(e, this.tempFilePathEventMap);
+            }
+          }
+        } catch (error) {
+          console.log(error);
+        }
+      },
+      getTempFilePath(params) {
+        if (!params?.file || !params?.file?.name) {
+          throw Error('ERROR: file is undefined');
+        }
+        // #ifndef MP
+        this.initHandle({
+          params,
+          handleType: 'getTempFilePath',
+          ob: this.tempFilePathEventMap,
+        });
+        // #endif
+        // #ifdef MP
+        try {
+          if (params?.success && typeof params?.success === 'function') {
+            return params.success({
+              name: params.file.name,
+              result: params.file.path,
+            });
+          }
+        } catch (error) {
+          if (params?.fail && typeof params?.fail === 'function') {
+            return params.fail(error);
+          }
+        }
+
+        // #endif
+      },
+      processQueue() {
+        if (this.isUpdating || this.uploadQueue.length === 0) {
+          return;
+        }
+        this.isUpdating = true;
+        const form = this.uploadQueue.shift();
+        form._uuid = `${+new Date()}-${Math.random()}`;
+        setTimeout(() => {
+          this.uploadParams = JSON.stringify(form);
+          this.$nextTick(() => {
+            this.isUpdating = false;
+            this.processQueue();
+          });
+        }, 300);
+      },
+      upload(params) {
+        // #ifndef MP
+        try {
+          if (!params?.url) {
+            throw Error('upload ERROR: url is undefined');
+          }
+          if (!params?.file || !params?.file?.name) {
+            throw Error('upload ERROR: file is undefined');
+          }
+          const { success, onprogress, fail, ...form } = params;
+
+          const callFunctions = this.getCallBackFn(params, ['success', 'fail', 'onprogress']);
+          this.uploadEventMap.set(params.file.name, callFunctions);
+          // 将当前上传任务加入队列
+          this.uploadQueue.push(form);
+          // 开始处理队列
+          this.processQueue();
+        } catch (error) {
+          console.error(error.message);
+          if (params?.fail && typeof params?.fail === 'function') {
+            return params.fail(error);
+          }
+        }
+        // #endif
+        // #ifdef MP
+        return this.wxUpload(params);
+        // #endif
+      },
+      wxUpload(params) {
+        // #ifdef MP
+        try {
+          if (!params?.url) {
+            throw Error('upload ERROR: url is undefined');
+          }
+          if (!params?.file || !params.file?.name || !params?.file?.path) {
+            throw Error('upload ERROR: file is undefined');
+          }
+
+          const { success, onprogress, fail, url, file, ...form } = params;
+
+          let xmlRequest = uni.uploadFile({
+            url,
+            filePath: file.path,
+            success: e => {
+              this.uploadEventMap.delete(params.file.name);
+              return success(e);
+            },
+            fail: e => {
+              this.uploadEventMap.delete(params.file.name);
+              return fail(e);
+            },
+            ...form,
+          });
+          this.uploadEventMap.set(params.file.name, {
+            xmlRequest,
+          });
+          xmlRequest.onProgressUpdate(({ progress = 0 }) => {
+            if (progress <= 100) {
+              onprogress({
+                name: params.file.name,
+                progress,
+              });
+            }
+          });
+        } catch (error) {
+          console.error(error.message);
+          if (params?.fail && typeof params?.fail === 'function') {
+            return params.fail(error);
+          }
+        }
+
+        // #endif
+      },
+      uploadCallBack(e) {
+        try {
+          this.executeCallBackFn(e, this.uploadEventMap, ['success', 'fail']);
+          this.uploadEventMap.delete(e.name);
+        } catch (error) {
+          console.error('upload', error);
+        }
+      },
+      uploadProgress(e) {
+        try {
+          this.debug && console.log('上传中', e);
+          this.executeCallBackFn(e, this.uploadEventMap, ['onprogress']);
+        } catch (error) {
+          console.error('Progress', error);
+        }
+      },
+      changeFile(files) {
+        if (files?.length) {
+          if (this.chooseFileSuccess && typeof this.chooseFileSuccess === 'function') {
+            this.chooseFileSuccess(files);
+          }
+        } else if (this.chooseFileFail && typeof this.chooseFileFail === 'function') {
+          this.chooseFileFail({
+            status: 'fail',
+            files: [],
+          });
+        }
+        this.inputHtml = '';
+      },
+      openDocument({ file, ...params }) {
+        if (!file) {
+          throw '未传入文件';
+        }
+        this.getTempFilePath({
+          file,
+          success: e => {
+            // #ifndef H5
+            uni.openDocument({
+              filePath: e.result,
+              success: res => {
+                if (params?.success && typeof params?.success === 'function') {
+                  return params.success(res);
+                }
+              },
+              fail: error => {
+                console.error('打开文档失败', error);
+                if (params?.fail && typeof params?.fail === 'function') {
+                  return params.fail(error);
+                }
+              },
+            });
+            // #endif
+            // #ifdef H5
+            window.open(e.result, '_blank');
+            // #endif
+          },
+          fail: error => {
+            console.error('打开文档失败', error);
+            if (params?.fail && typeof params?.fail === 'function') {
+              return params.fail(error);
+            }
+          },
+        });
+      },
+      toToast(msg) {
+        if (msg) {
+          uni.showToast({
+            title: msg,
+            icon: 'none',
+            duration: 3000,
+          });
+        }
+      },
+    },
+  };
+</script>
+
+<script lang="renderjs" module="render">
+  class DefOpitons {
+  	constructor() {
+  		// 本次选择文件个数上限
+  		this.count = 9;
+  		// 单个文件大小上限
+  		this.size = 10;
+  		// input file原生属性
+  		this.accept = '';
+  		// 是否允许多选
+  		this.multiple = true;
+  		// 允许选择的格式,空等于不限制,多个以逗号隔开,例如 'png,jpg,jpeg'
+  		this.formats = '';
+  	}
+  }
+
+  export default {
+  	data() {
+  		return {
+  			configs: {},
+  			options: {},
+  			fileDom: null,
+  			files: new Map(),
+  			xmls: new Map()
+  		}
+  	},
+  	methods: {
+  		initConfig(config) {
+  			this.configs = config;
+  		},
+  		generalFail({
+  			name,
+  			methodName
+  		}, error) {
+  			this.$ownerInstance.callMethod(methodName, {
+  				name,
+  				status: 'fail',
+  				message: error?.message || error
+  			});
+  		},
+  		renderFileHandle(e) {
+  			switch (e?.type) {
+  				case 'deleted':
+  					if (e?.name) {
+  						this.configs?.debug && console.log('删除文件:', e.name);
+  						this.files.delete(e.name);
+  					}
+  					break;
+  				case 'clear':
+  					this.configs?.debug && console.log('清空文件');
+  					this.files.clear();
+  					break;
+  				case 'getBase':
+  					this.executeToBase(e);
+  					break;
+  				case 'getArrayBuffer':
+  					this.executeToArrayBuffer(e);
+  					break;
+  				case 'getTempFilePath':
+  					this.executeToTempFilePath(e);
+  					break;
+  				case 'asyncFileTempFilePath':
+  					this.files.get(e.name)['tempFilePath'] = e?.params?.tempFilePath;
+  					break;
+  				case 'abort':
+  					if (this.xmls.get(e.name)) {
+  						this.xmls.get(e.name)?.abort();
+  						console.log('已终止上传' + e.name);
+  					}
+  					break;
+  				default:
+  					break;
+  			}
+  		},
+  		async executeToTempFilePath(e) {
+  			const methodName = 'getTempFilePathCallBack';
+  			try {
+  				if (!this.files.has(e?.name)) {
+  					throw '文件未找到';
+  				}
+  				if (this.files.get(e.name)?.tempFilePath) {
+  					const result = {
+  						tempFilePath: this.files.get(e.name).tempFilePath
+  					}
+  					this.$ownerInstance.callMethod(methodName, {
+  						name: e.name,
+  						status: 'success',
+  						result
+  					})
+  					return;
+  				}
+  				this.executeToArrayBuffer(e, methodName);
+  			} catch (error) {
+  				console.error(error);
+  				this.generalFail({
+  					name: e.name,
+  					methodName
+  				}, error);
+  			}
+
+  		},
+  		async executeToArrayBuffer(e, methodName = 'getArrayBufferCallBack') {
+  			try {
+  				const fileType = this.files.get(e.name)?.file?.type;
+  				const result = await this.fileToArrayBuffer(e?.name);
+  				// 分片参数配置
+  				const chunkSize = 0.5 * 1204 * 1024; // 每片0.5M
+  				const totalChunks = Math.ceil(result.length / chunkSize); // 总分片数
+  				for (let i = 0; i < totalChunks; i++) {
+  					const start = i * chunkSize;
+  					const end = Math.min(start + chunkSize, result.length);
+  					const chunk = result.slice(start, end); // 当前分片数据
+
+  					this.$ownerInstance.callMethod(methodName, {
+  						name: e.name,
+  						fileType,
+  						status: 'success',
+  						result: {
+  							chunk, // 当前分片数据
+  							index: i, // 当前分片索引
+  							totalChunks, // 总分片数
+  						}
+  					});
+  				}
+  			} catch (error) {
+  				console.error(error);
+  				this.generalFail({
+  					name: e.name,
+  					methodName
+  				}, error);
+  			}
+  		},
+  		async executeToBase(e, methodName = 'getBaseCallBack') {
+  			try {
+  				const fileType = this.files.get(e.name)?.file?.type;
+  				const result = await this.fileToBase(e?.name);
+  				// 分片参数配置
+  				const chunkSize = 0.5 * 1204 * 1024; // 每片0.5M
+  				const totalChunks = Math.ceil(result.length / chunkSize); // 总分片数
+
+  				for (let i = 0; i < totalChunks; i++) {
+  					const start = i * chunkSize;
+  					const end = Math.min(start + chunkSize, result.length);
+  					const chunk = result.slice(start, end); // 当前分片数据
+
+  					this.$ownerInstance.callMethod(methodName, {
+  						name: e.name,
+  						fileType,
+  						status: 'success',
+  						result: {
+  							chunk, // 当前分片数据
+  							index: i, // 当前分片索引
+  							totalChunks, // 总分片数
+  						}
+  					});
+  				}
+  			} catch (error) {
+  				console.error(error);
+  				this.generalFail({
+  					name: e.name,
+  					methodName
+  				}, error);
+  			}
+  		},
+  		chooseFileHandle(e) {
+  			if (!e?.html) {
+  				return;
+  			}
+  			this.options = Object.assign(new DefOpitons, e.option);
+  			if (Number(this.options?.count) < 1) {
+  				this.$ownerInstance.callMethod('changeFile', []);
+  				return;
+  			}
+  			this.configs?.debug && console.log('chooseFile', this.options);
+
+  			this.$nextTick(() => {
+  				this.fileDom = document.getElementById(`lsj-file-${e.id}`);
+  				this.fileDom.accept = this.options.accept;
+  				this.fileDom.multiple = Number(this.options?.count) > 1 ? this.options.multiple : false;
+  				this.fileDom.value = '';
+  				this.fileDom.onchange = async (event) => {
+  					const Files = event.target.files;
+  					let newFiles = [];
+  					for (let i = 0, len = Files.length; i < len; i++) {
+  						const file = Files[i];
+  						if (this.options?.count && newFiles.length >= Number(this.options.count)) {
+  							this.$ownerInstance.callMethod('toToast', `只允许选择${this.options.count}个文件`);
+  							this.fileDom.value = '';
+  							break;
+  						}
+  						try {
+  							newFiles.push(await this.addFile(file));
+
+  						} catch (error) {
+  							console.error(error);
+  						}
+  					}
+  					this.$ownerInstance.callMethod('changeFile', newFiles);
+  				};
+  				this.fileDom.click();
+
+  			});
+
+
+  		},
+  		addFile(file) {
+  			return new Promise((resolve, reject) => {
+  				if (file) {
+  					let fileName = file.name;
+  					let newFileName = fileName;
+  					// 限制文件格式
+  					const suffix = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
+  					const formats = this.options.formats && (this.options.formats.toLowerCase());
+  					this.configs?.debug && console.log('添加文件', fileName, '大小', file.size, '格式', suffix);
+  					if (formats && !formats.includes(suffix)) {
+  						this.$ownerInstance.callMethod('toToast', `不支持上传${suffix.toUpperCase()}格式文件`);
+  						return reject();
+  					}
+  					// 限制文件大小
+  					if (file.size > 1024 * 1024 * Math.abs(this.options.size)) {
+  						this.$ownerInstance.callMethod('toToast', `文件大小请勿超过${this.options.size}M`);
+  						return reject();
+  					}
+  					try {
+  						if (!this.configs?.distinct) {
+  							while (this.files.has(newFileName)) {
+  								const lastDotIndex = fileName.lastIndexOf('.');
+  								let nameWithoutExt = fileName.substring(0, lastDotIndex);
+  								let extension = fileName.substring(lastDotIndex);
+  								let counter = 1;
+  								while (this.files.has(`${nameWithoutExt}(${counter})${extension}`)) {
+  									counter++;
+  								}
+  								newFileName = `${nameWithoutExt}(${counter})${extension}`;
+  							}
+  						}
+  					} catch (e) {
+  						newFileName = Date.now() + '_' + newFileName;
+  					}
+  					this.files.set(newFileName, {
+  						file,
+  						name: newFileName,
+  						size: file.size,
+  						progress: 0,
+  						status: 'waiting'
+  					});
+  					return resolve(this.files.get(newFileName));
+
+  				}
+  			})
+  		},
+  		fileToArrayBuffer(name) {
+  			return new Promise((resolve, reject) => {
+  				if (!this.files.has(name)) {
+  					throw '文件未找到';
+  				}
+  				if (this.files.get(name)?.base64) {
+  					this.configs?.debug && console.log('读取缓存二进制');
+  					return resolve(this.files.get(name).base64.split(',')[1]);
+  				}
+  				const file = this.files.get(name).file;
+  				const reader = new FileReader();
+  				reader.onload = (e) => {
+  					let base64 = e.target.result;
+  					let item = this.files.get(name);
+  					item['base64'] = base64;
+  					this.files.set(name, item);
+  					resolve(base64.split(',')[1])
+  				};
+  				reader.onerror = (e) => reject(e);
+  				reader.readAsDataURL(file);
+  			});
+  		},
+  		fileToBase(name) {
+  			return new Promise((resolve, reject) => {
+  				if (!this.files.has(name)) {
+  					throw '文件未找到';
+  				}
+  				// 已转过base无需重复转换
+  				if (this.files.get(name)?.base64) {
+  					this.configs?.debug && console.log('读取缓存base64');
+  					return resolve(this.files.get(name).base64.replace(/[\r\n]/g, ""));
+  				}
+  				const file = this.files.get(name).file;
+  				const reader = new FileReader();
+  				reader.onload = (e) => {
+  					let base64 = e.target.result;
+  					let item = this.files.get(name);
+  					item['base64'] = base64;
+  					this.files.set(name, item);
+  					resolve(base64.replace(/[\r\n]/g, ""));
+  				};
+  				reader.onerror = (e) => reject(e);
+  				reader.readAsDataURL(file);
+  			})
+  		},
+  		isContentTypeJson(header) {
+  			try {
+  				const contentType = header && header['Content-Type'];
+  				return contentType && contentType.includes('application/json');
+  			} catch (error) {
+  				return false
+  			}
+  		},
+  		async uploadHandle(param) {
+  			const params = JSON.parse(param);
+  			if (!params.file?.name) {
+  				return;
+  			}
+  			if (params.file?.status === 'onprogress') {
+  				console.error('该文件状态为上传中,请勿重复提交,若不需要此过滤,请前往插件源码删除该判断');
+  				return;
+  			}
+  			this.configs?.debug && console.log('params', params);
+
+  			if (!this.files.has(params.file?.name)) {
+  				this.$ownerInstance.callMethod('uploadCallBack', {
+  					name: params.file.name,
+  					status: 'fail',
+  					message: '文件未找到',
+  				});
+  				return;
+  			}
+  			const {
+  				url,
+  				name = 'file',
+  				method = 'post',
+  				toBase = false,
+  				header = {},
+  				formData = {},
+  			} = params;
+
+  			let form = new FormData();
+  			for (let keys in formData) {
+  				form.append(keys, formData[keys]);
+  			}
+
+  			if (toBase) {
+  				const base64 = await this.fileToBase(params.file.name);
+  				form.append(name, base64);
+  			} else {
+  				const file = this.files.get(params.file.name).file;
+  				form.append(name, file);
+  			}
+  			let xmlRequest = new XMLHttpRequest();
+  			this.xmls.set(params.file.name, xmlRequest);
+  			xmlRequest.upload.addEventListener(
+  				'progress',
+  				event => {
+  					if (event.lengthComputable) {
+  						let progress = Math.ceil((event.loaded * 100) / event.total)
+  						if (progress <= 100) {
+  							this.$ownerInstance.callMethod('uploadProgress', {
+  								status: 'onprogress',
+  								name: params.file.name,
+  								progress
+  							});
+  						}
+  					}
+  				},
+  				false
+  			);
+  			xmlRequest.ontimeout = () => {
+  				console.error('请求超时')
+  				this.$ownerInstance.callMethod('uploadCallBack', {
+  					name: params.file.name,
+  					status: 'fail',
+  					message: '请求超时'
+  				});
+  				this.xmls.delete(params.file.name);
+  			}
+  			xmlRequest.onerror = event => {
+  				console.error(event);
+  			};
+  			xmlRequest.onreadystatechange = event => {
+  				if (xmlRequest.readyState == 4) {
+  					this.configs?.debug && console.log('接口是否支持跨域', xmlRequest.withCredentials);
+  					if (xmlRequest.status == 200) {
+  						this.configs?.debug && console.log('上传完成:' + xmlRequest.responseText)
+  						this.$ownerInstance.callMethod('uploadCallBack', {
+  							name: params.file.name,
+  							status: 'success',
+  							message: '上传完成',
+  							result: xmlRequest.responseText,
+  						});
+  						this.xmls.delete(params.file.name);
+  						return;
+  					}
+  					console.error('ERROR:上传失败 HTTP status:' + xmlRequest.status)
+  					this.xmls.delete(params.file.name);
+  					this.$ownerInstance.callMethod('uploadCallBack', {
+  						name: params.file.name,
+  						status: 'fail',
+  						message: '上传失败',
+  						result: xmlRequest.status
+  					});
+  				}
+  			}
+  			xmlRequest.open(method, url, true);
+  			for (let keys in header) {
+  				xmlRequest.setRequestHeader(keys, header[keys])
+  			}
+  			if (this.isContentTypeJson(header)) {
+  				const formObject = {};
+  				for (let [key, value] of form.entries()) {
+  					formObject[key] = value;
+  				}
+  				xmlRequest.send(JSON.stringify(formObject))
+  			} else {
+  				xmlRequest.send(form)
+  			}
+  		}
+  	}
+  }
+</script>
+<style scoped>
+  .lsj-upload {
+    position: fixed;
+    z-index: -1;
+    top: -100px;
+  }
+</style>

+ 79 - 0
uni_modules/lsj-upload/package.json

@@ -0,0 +1,79 @@
+{
+    "id": "lsj-upload",
+    "displayName": "全文件上传选择非原生3.0版",
+    "version": "3.0.0",
+    "description": "文件选择上传-支持APP-H5网页-微信小程序",
+    "keywords": [
+        "附件",
+        "file",
+        "upload",
+        "上传",
+        "文件管理器"
+    ],
+    "repository": "",
+    "engines": {
+    },
+    "dcloudext": {
+        "sale": {
+            "regular": {
+                "price": "0.00"
+            },
+            "sourcecode": {
+                "price": "0.00"
+            }
+        },
+        "contact": {
+            "qq": ""
+        },
+        "declaration": {
+            "ads": "无",
+            "data": "无",
+            "permissions": "文件读取/相机/相册读取"
+        },
+        "npmurl": "",
+        "type": "component-vue"
+    },
+    "uni_modules": {
+        "platforms": {
+            "cloud": {
+                "tcb": "y",
+                "aliyun": "y",
+                "alipay": "n"
+            },
+            "client": {
+                "App": {
+                    "app-vue": "y",
+                    "app-nvue": "y"
+                },
+                "H5-mobile": {
+                    "Safari": "y",
+                    "Android Browser": "y",
+                    "微信浏览器(Android)": "y",
+                    "QQ浏览器(Android)": "y"
+                },
+                "H5-pc": {
+                    "Chrome": "y",
+                    "IE": "y",
+                    "Edge": "y",
+                    "Firefox": "y",
+                    "Safari": "y"
+                },
+                "小程序": {
+                    "微信": "y",
+                    "阿里": "u",
+                    "百度": "u",
+                    "字节跳动": "u",
+                    "QQ": "u"
+                },
+                "快应用": {
+                    "华为": "y",
+                    "联盟": "y"
+                },
+                "Vue": {
+                    "vue2": "y",
+                    "vue3": "y"
+                }
+            }
+        }
+    }
+}

+ 590 - 0
uni_modules/lsj-upload/readme.md

@@ -0,0 +1,590 @@
+# lsj-upload 全文件选择上传
+
+
+## 右侧按钮导入插件或导入示例项目,导入的是(2.0版 H5+小程序免费),现插件已推出3.0版,完全API式调用选择器,支持pdf、doc、zip...等所有类型文件!
+# 购买须知!!!
+本插件为原创作品,已申请软件著作权保护。为尊重开发者权益,请勿未经授权转售或传播。感谢您的理解与支持!
+
+## 【1.0版(性能低功能少)】 vs 【2.0版(操作复杂)】 vs 【3.0版(功能全面,操作简单,物超所值)】
+#### 本插件适用App [ Android、iOS ]、 微信小程序、H5应用
+#### 支持所有格式文件选择和上传,使用过程中有任何问题可im联系我或添加我微信,会第一时间回复您
+#### 若未及时回复可联系我wx号:lsj274962262
+#### 感谢支持!!!
+| 版本		| 使用说明		|  赞助费用 | 免费下载地址 	|
+| --------- | ------------ 	| ------:  | -----------|
+| 1.0版		| [1.0版文档](https://ext.dcloud.net.cn/plugin?id=1015) | 全端免费  |	[1.0版(免费)](https://ext.dcloud.net.cn/plugin?id=1015) | 
+| 2.0版		| [2.0版文档](#v2) | App端9.9元  	  |	[2.0版(H5+小程序免费)](https://ext.dcloud.net.cn/plugin?id=5459) | 
+| 3.0版(不支持nvue)		| [3.0版文档](#v3) | 19.9元(限时)   |	微·信:lsj274962262 	| 
+------
+
+# <a id="v3">3.0版文档</a>
+## API式调用选择器,支持pdf、doc、zip...等所有类型文件
+
+
+## props
+| 属性		| 是否必填	|  值类型	| 默认值	| 说明			|
+| --------- | -------- 	| -----: 	| --: 	| :------------ |
+| debug		|	否 		| Boolean	|false	| 开启调试日志					|
+| distinct	|	否 		| Boolean	|false	| 同名文件是否覆盖					|
+
+## API
+| 方法名				| 参数								|  支持平台			| 说明			|
+| ----------------- | ----------------------------- 	| --------------	| :------------ |
+| chooseFile		| [chooseFile参数说明](#chooseFileParams) 	| App/H5/微信小程序	| 文件选择(微信小程序是用的wx.chooseMessageFile)		|
+| upload			| [upload参数说明](#uploadParams) 			| App/H5/微信小程序	| 文件上传		|
+| getTempFilePath	| [文件处理参数说明](#fileParams) 				| App/H5/微信小程序	| 获取文件临时路径		|
+| getBase			| [文件处理参数说明](#fileParams) 				| App/H5/微信小程序	| 获取文件base64		|
+| getArrayBuffer	| [文件处理参数说明](#fileParams) 				| App/H5/微信小程序	| 获取文件ArrayBuffer	|
+| openDocument		| [文件处理参数说明](#fileParams) 				| App/H5/微信小程序	| 打开文件			|
+| abort				| file						 				| App/H5/微信小程序	| 中断上传			|
+| deleted			| file						 				| App/H5/微信小程序	| 删除文件			|
+| clear				| -	 										| App/H5/微信小程序	| 清空文件			|
+
+### <a id="chooseFileParams">chooseFile参数说明 Object</a>
+| 参数名		| 类型		|  必填	| 默认值	 | 说明			|
+| --------- | -------- 	| -----:|-------:| :------------ |
+| count		| Number 	| 否		| 9		 | 允许选择的文件个数					|
+| size		| Number 	| 否		| 10	 | 单个文件大小上限,单位兆(M)			|
+| multiple	| Boolean 	| 否		| true	 | 是否允许多选(count=1时强制禁止多选)	|
+| formats	| String 	| 否		| -		 | 允许选择的格式,空串等于不限制,多个以逗号隔开,例如 'png,jpg,jpeg'	|
+| accept	| String 	| 否		| -		 | 原生input file属性,控制系统弹框可选文件类型展示					|
+| success	| Function 	| 否		| -		 | 成功回调														|
+| fail		| Function 	| 否		| -		 | 失败回调														|
+
+### <a id="uploadParams">upload参数说明 Object</a>
+| 参数名		| 类型		|  必填	| 默认值	 | 说明			|
+| --------- | -------- 	| -----:|-------:| :------------ |
+| file		| Object 	| 是		| -		 | chooseFile返回的file						|
+| url		| String 	| 是		| -		 | 上传文件的服务端接口地址						|
+| name		| String 	| 否		| 'file' | 服务端接收文件字段的key						|
+| method	| String 	| 否		| 'post' | 请求协议类型								|
+| header	| Object 	| 否		| -		 | HTTP 请求 Header							|
+| formData	| Object 	| 否		| -		 | HTTP 请求中其他额外的 form data									|
+| onprogress| Function 	| 否		| -		 | 上传进度回调(持续)												|
+| success	| Function 	| 否		| -		 | 成功回调														|
+| fail		| Function 	| 否		| -		 | 失败回调														|
+
+### <a id="fileParams">文件处理参数说明</a>
+| 参数名		| 类型		|  必填	| 默认值	 | 说明			|
+| --------- | -------- 	| -----:|-------:| :------------ |
+| file		| Object 	| 是		| -		 | chooseFile返回的file						|
+| success	| Function 	| 否		| -		 | 成功回调														|
+| fail		| Function 	| 否		| -		 | 失败回调														|
+
+
+
+## 3.0版示例
+
+```vue
+<template>
+	<view>
+		<lsj-upload debug ref="lsjUploadRef"></lsj-upload>
+		
+		<view style="padding: 60rpx 30rpx;">
+			<view style="color: orangered;">
+				<view style="font-weight: 600;">lsj-upload3.0版</view>API式调用选择器,支持pdf、doc、zip...等所有类型文件
+			</view>
+			<view>API</view>
+			<view>chooseFile: 选择文件</view>
+			<view>upload: 上传文件</view>
+			<view>abort: 终止上传</view>
+			<view>getTempFilePath: 获取文件临时路径</view>
+			<view>getBase: 获取文件base64</view>
+			<view>getArrayBuffer: 获取文件ArrayBuffer</view>
+			<view>openDocument: 打开文件</view>
+			<view>deleted: 删除文件</view>
+			<view>clear: 清空文件</view>
+		</view>
+		
+		<view style="padding: 8rpx 60rpx;">
+			<button @click="onClick">chooseFile (选择文件)</button>
+		</view>
+		
+		<view style="margin-top: 60rpx;padding: 24rpx;">
+			<view>已选文件返回files数组,自己写样式</view>
+			
+			<view v-for="(item,index) in files" :key="index">
+				<text>文件名:{{ item.name }}</text>
+				<text style="margin-left: 10rpx;">大小:{{ item.size }}</text>
+				<text style="margin-left: 10rpx;">状态:{{ item.status }}</text>
+				<text style="margin-left: 10rpx;">进度:{{ item.progress }}</text>
+				<text v-if="item.responseText" style="margin-left: 10rpx;">服务端数据:{{ item.responseText }}</text>
+				<view class="flex">
+					<text class="flex-item" @click="uploadHandle(item)">上传文件</text>
+					<text class="flex-item" @click="onAbort(item)">终止上传</text>
+					<text class="flex-item" @click="getBase(item)">获取Base64</text>
+					<text class="flex-item" @click="getArrayBuffer(item)">获取ArrayBuffer</text>
+					<text class="flex-item" @click="getTempFilePath(item)">获取临时路径</text>
+					<text class="flex-item" @click="openDocument(item)">打开文件</text>
+					<text class="flex-item" @click="del(index)">删除文件</text>
+				</view>
+			</view>
+		</view>
+		
+		<view style="padding: 60rpx 30rpx;">
+			<div style="color: blue;" @click="clear">clear (清空所有文件)</div>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				title: 'Hello',
+				files: [],
+				base64: '',
+				tempFilePath: ''
+			}
+		},
+		onLoad() {
+
+		},
+		methods: {
+			// 选择文件
+			onClick() {
+				this.$refs.lsjUploadRef.chooseFile({
+					count: 3, // 文件可选个数
+					size: 10, // 文件大小上限兆(M)
+					success:(files) => {
+						this.files = this.files.concat(files);
+						console.log('files',files);
+						// 这个file不是文件流,是插件内需要的file对象,调用插件的其他方法时需传入这个
+						this.files.forEach(file=> {
+							// 上传到服务器
+							if (file.status === 'waiting') {
+								// 上传
+								// this.uploadHandle(file);
+							}
+						})
+					},
+					fail:(err) => {
+						console.log('选择文件失败');
+					}
+				})
+			},
+			// 上传到服务器
+			uploadHandle(file) {
+				this.$refs.lsjUploadRef.upload({
+					url: 'http://ttest.shlink.com/ts/v1/oss/upload', // 更换成你自己接口地址
+					file,
+					name: 'file',
+					method: 'post',
+					header: {
+						Authorization: ''
+					},
+					formData: {},
+					onprogress:(e)=> {
+						file.status = e.status;
+						file.progress = e.progress;
+						console.log('上传中:',e.progress);
+					},
+					success:(e) => {
+						file.status = 'success';
+						console.log('上传成功',e);
+					},
+					fail:(e) => {
+						file.status = 'fail';
+						file.progress = 0;
+						console.log('上传失败',e);
+					}
+				});
+			},
+			// 获取文件TempFilePath
+			getTempFilePath(file) {
+				this.$refs.lsjUploadRef.getTempFilePath({
+					file,
+					success:(e) => {
+						console.log(e.result);
+						// 图片回显示例
+						// #ifdef H5
+						// h5端
+						// 如果希望回显图片可以用URL.createObjectURL获取blob地址后再image标签src
+						this.tempFilePath = URL.createObjectURL(e.result);
+						// #endif
+						// 其他端不需要URL.createObjectURL
+						// #ifndef H5
+							this.tempFilePath = e.result;
+						// #endif
+						
+						
+						// 临时路径用于uni.uploadFile上传的示例
+						uni.uploadFile({
+							url: 'http://ttest.shlink.com/ts/v1/oss/upload', // 更换成你自己接口地址
+							header: {
+								Authorization: 'bearer '
+							},
+							success:(e) => {
+								console.log('success',e);
+							},
+							fail: (err) => {
+								console.log('fail',err);
+							},
+							// #ifdef H5
+							file: e.result
+							// #endif
+							// #ifndef H5
+							filePath: e.result
+							// #endif
+						})
+					}
+				});
+			},
+			// 获取文件base64
+			getBase(file) {
+				this.$refs.lsjUploadRef.getBase({
+					file,
+					success:(e) => {
+						console.log('getBase',e);
+						// #ifndef MP
+							this.base64 = e.result;
+						// #endif
+						// #ifdef MP
+							this.base64 = 'data:image/png;base64,' + e.result;
+						// #endif
+					}
+				});
+			},
+			// 获取文件ArrayBuffer
+			getArrayBuffer(file) {
+				this.$refs.lsjUploadRef.getArrayBuffer({
+					file,
+					success:(e) => {
+						console.log('getArrayBuffer',e);
+					}
+				});
+			},
+			// 打开文件
+			openDocument(file) {
+				this.$refs.lsjUploadRef.openDocument({
+					file,
+					success:(e) => {
+						console.log('打开文件成功');
+					}
+				});
+			},
+			// 终止上传文件
+			onAbort(file) {
+				if (file.status === 'onprogress') {
+					this.$refs.lsjUploadRef.abort(file);
+					file.progress = 0;
+					console.log('已终止上传:',file.name);
+				}
+			},
+			// 删除指定文件
+			del(index) {
+				this.$refs.lsjUploadRef.deleted(this.files[index]);
+				this.files.splice(index,1);
+			},
+			// 清空所有文件
+			clear() {
+				this.$refs.lsjUploadRef.clear();
+				this.files = [];
+			},
+		}
+	}
+</script>
+<style scoped>
+	.flex {
+		display: flex;
+		flex-wrap: wrap;
+		gap: 10px;
+	}
+	.flex-item {
+		flex: 0 0 calc(33.333% - 10px);
+		box-sizing: border-box;
+		border: 1px solid blue;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		min-height: 2em;
+	}
+</style>
+
+
+
+```
+
+------------------------------------------------
+-
+-
+-
+-
+# <a id="v2">2.0版文档</a>
+## 插件使用注意事项
+#### 2.0版通过webview实现,不支持返回文件的本地路径,上传文件必须使用组件内置的upload函数
+#### 组件如果在scroll-view内使用需要自己监听scroll事件,并在滚动结束的时候调用一次show(查看下方scroll示例)!!
+#### 控件的height、width应与slot自定义内容宽高度保持一致。
+#### nvue窗口只能使用固定模式position=absolute 。
+#### show() 当DOM重排后在this.$nextTick内调用show(),控件定位会更加准确。
+#### hide() APP端webview层级比view高,如不希望触发点击时,应调用hide隐藏控件,反之调用show。
+#### APP端请优先联调Android,上传成功后再运行iOS端。
+#### 若iOS端跨域服务端同学实在配置不好,可把hybrid下html目录放到服务器去,同源则不存在跨域问题,或者toBase传true(以base64格式上传)。
+#### 小程序端因hybrid不能使用本地HTML,所以插件提供的是从微信消息列表拉取文件并选择,请知悉。
+------
+## 上传报status:0 问题排查
+### App如果上传报status: 0,调试方法:关闭manifest.json>h5的代理(若有配置代理),并运行项目至Chrome浏览器,按F12查看接口详细报错信息(重点:是Chrome,不是HX内置浏览器)
+### Android端不存在跨域问题,如上传报status: 0时
+  #### 1、如果能进入接口,但收不到任何参数,检查是否在option>header里传入了Content-Type属性(默认不需要传,会自动匹配,若接口要求必须传则需要与接口匹配)
+  #### 2、如果未能进入接口,检查nginx网关配置,不清楚可加群查看群相册资料。
+### iOS端若出现status:0,排除上面两个问题后只会是跨域问题
+  #### 1、后端处理允许上传接口跨域。(前端无需修改)
+  #### 2、若后端不放开跨域,可将hybrid>html文件夹放入与接口同源的服务器,同源访问则不存在跨域问题。(可加群查看群相册资料)
+------
+## 使用说明
+| 属性		| 是否必填	|  值类型	| 默认值	| 说明			|
+| --------- | -------- 	| -----: 	| --: 	| :------------ |
+| permission|	否 		| String	|组件内查看	| 需要校验的系统权限集合					|
+| isPermissionInToast|	否 		| Boolean	|true	| 未授权时是否显示内置Toast提示权限permission[].message,建议传true	|
+| isPermissionInModal|	否 		| Boolean	|true	| 用户拒绝授权时是否显示内置弹框引导用户开启权限	|
+| width		|	否 		| String	|100%	| 容器宽度(App必填且与slot内容宽度一致)		|
+| height	|	是 		| String	|80rpx	| 容器高度(App必填且与slot内容高度一致)		|
+| debug		|	否 		| Boolean	|false	| 打印调试日志,传true时会将透明层显示(app端显示黑色的块、h5显示input),便于观察调试	|
+| option	|	是 		| Object	|-		| [文件上传接口相关参数](#p1)|
+| toBase	|	否 		| Boolean	|false	| 【2.3.8新增】选择的文件是否转base64上传|
+| instantly	|	否 		| Boolean	|true	| instantly=true:选择文件后自动上传,instantly=false不自动上传,可通过调用upload()函数上传所有待上传的文件|
+| distinct	|	否 		| Boolean	|false	| 【2.3.0新增】是否去重同名文件(提示:不去重复时前端展示同名文件名称后面+(N),后端接收到的file.name还是为原名)|
+| count		|	否 		| Number	|10		| 文件选择上限(个)|
+| size		|	否 		| Number	|10		| 文件大小上限(M)	|
+| multiple	|	否 		| Boolean	|true	| 是否允许多选	|
+| wxFileType|	否 		| String	|all	| 微信小程序文件选择器格式限制(all=从所有文件选择,video=只能选择视频文件,image=只能选择图片文件,file=可以选择除了图片和视频之外的其它的文件)|
+| accept	|	否 		| String	|-		| 文件选择器input file格式限制(有效值自行百度查accept)|
+| formats	|	否 		| String	|-		| 限制允许上传的格式,空串=不限制,默认为空,多个格式以逗号隔开,例如png,jpg,pdf|
+| childId	|	否 		| String	|lsjUpload| 自定义组件id,在回调函数第二个参数返回(若页面有多个组件可用于区分是哪个组件回调)|
+| position	|	否 		| String	|static	| 控件的定位模式(static=控件随页面滚动;absolute=控件在页面中绝对定位,不随窗口内容滚动)|
+| top,left,right,bottom	|	否 		| [Number,String]	|0		| 设置控件绝对位置,position=absolute时有效|
+| @changeFile	|	否 		| Function	|Map	| 选择文件后触发,返回所有已选择文件Map集合|
+| @progress	|	否 		| Function	|Object	| 上传中持续触发,返回正在上传的文件对象,可通过set更新至页面显示上传进度|
+| @uploadEnd|	否 		| Function	|Object	| 上传结束回调,返回当前上传的文件对象,type等于success(上传成功)返回responseText属性(服务端返回数据)|
+| @permissionBefore|	否 		| Function	|Object	| 某项权限未授权时触发回调,返回{permission,message},仅isPermissionInToast传false时生效 |
+| @permissionFail|	否 		| Function	|Object	| 用户拒绝授权时触发回调,返回{permission,message,result} |
+## 【2.3.0】新增uni.$emit事件监听
+| 方法名			|  说明							 	|
+|---- 			| ----								|
+| $upload-show  | 调用当前页面所有上传组件的show() 	|
+| $upload-hide  | 调用当前页面所有上传组件的hide() 	|
+使用示例
+``` javascript
+this.$nextTick(()=>{
+	// 更新当前页所有上传组件在页面中的位置
+	uni.$emit('$upload-show',{});
+})
+```
+
+## $Refs
+|作用 | 方法名| 传入参数|  说明|
+|---- | --------- | -------- | :--: |
+|显示和定位控件点击层| show|-| 控件显示状态下可触发点击|
+|隐藏控件点击层| hide|-| 控件隐藏状态下不触发点击|
+|动态设置文件列表| setFiles|[Array,Map] files| 传入格式请与组件选择返回格式保持一致,且name为必须属性,可查看下方演示|
+|动态更新参数| setData|[String] name,[any] value| name支持a.b 和 a[b],可查看下方演示|
+|移除选择的文件| clear|[String] name| 不传参数清空所有文件,传入文件name时删除该name的文件|
+|手动上传| upload|[String] name| 不传参数默认依次上传所有type=waiting的文件,传入文件name时不关心type是否为waiting,单独上传指定name的文件|
+
+## <a id="p1">option说明</a>
+|参数 | 是否必填 |  说明|
+|---- | ---- | :--: |
+|url  |	是	| 上传接口地址|
+|name| 否	|上传接口文件key,默认为file|
+|header| 否	|上传接口请求头|
+|formData| 否	|上传接口额外参数|
+
+## progress返回对象字段说明
+|字段 |  说明|
+|---- | :--: |
+|file | 文件对象|
+|name |文件名称|
+|size |文件大小|
+|path |用于image标签src属性回显图片|
+|type |文件上传状态:waiting(等待上传)、loading(上传中)、success(成功) 、fail(失败)|
+|responseText|上传成功后服务端返回数据(仅type为success时存在)|
+
+## 以下演示为vue窗口使用方式,nvue使用区别是必须传入控件绝对位置如top,bottom,left,right,且position只能为absolute,如不清楚可点击右侧导入示例项目有详细演示代码。
+
+### vue:
+``` javascript
+<view>
+	<view class="header-bg">
+		<view class="header">基本使用示例</view>
+	</view>
+	<view style="padding: 30rpx;" v-for="(v,i) in data" :key="'data_'+i">{{v}}</view>
+	<!-- #ifndef MP-WEIXIN -->
+	<view v-for="(item,index) in files.values()" :key="'file_'+index">
+		<image style="width: 100rpx;height: 100rpx;" :src="item.path" mode="widthFix"></image>
+		<text>{{ item.name }}</text>
+		<text style="margin-left: 10rpx;">大小:{{ item.size }}</text>
+		<text style="margin-left: 10rpx;">状态:{{ item.type }}</text>
+		<text style="margin-left: 10rpx;">进度:{{ item.progress }}</text>
+		<text v-if="item.responseText" style="margin-left: 10rpx;">服务端数据:{{ item.responseText }}</text>
+		<text style="margin-left: 10rpx;padding: 0 10rpx;border: 1rpx solid #007AFF;" @click="clear(item.name)">删除</text>
+	</view>
+	<!-- #endif -->
+	
+	<!-- #ifdef MP-WEIXIN -->
+	<view v-for="(item,index) in wxFiles" :key="'file_'+index">
+		<text>{{ item.name }}</text>
+		<text style="margin-left: 10rpx;">大小:{{ item.size }}</text>
+		<text style="margin-left: 10rpx;">状态:{{ item.type }}</text>
+		<text style="margin-left: 10rpx;">进度:{{ item.progress }}</text>
+		<view>
+			<button @click="clear(item.name)">删除</button>
+		</view>
+	</view>
+	<!-- #endif -->
+	
+	<lsj-upload 
+	ref="lsjUploadRef"
+	debug
+	:count="count"
+	:width="width"
+	:height="height"
+	:option="option"
+	:toBase="toBase"
+	@changeFile="onChange"
+	@progress="onprogress"
+	@uploadEnd="onuploadEnd"
+	>
+		<view style="background-color: #007AFF;" :style="{width: width,height: height}">自定义选择按钮</view>
+	</lsj-upload>
+	<view class="bottom-btn flex">
+		<button @click="add">在按钮上方添加数据</button>
+		<button @click="onsetFiles">setFiles同步文件</button>
+	</view>
+</view>
+
+
+```
+
+---
+* 函数说明
+
+
+``` javascript
+export default {
+	data() {
+		return {
+			data: ['DOM重排演示'],
+			option: {
+				// 上传服务器地址,该地址非真实路径,需替换为你项目自己的接口地址
+				url: 'http://iestest.com/dropbox/document/upload',
+				// 上传附件的key
+				name: 'file',
+				// 请求头,默认不要写content-type,让浏览器自适配
+				header: {},
+				// 额外参数
+				formData: {}
+			},
+			width: '180rpx',
+			height: '180rpx',
+			count: 9,
+			toBase: false,
+			// 文件回显列表
+			files: new Map(),
+		}
+	},
+	onReady() {
+		// 模拟异步请求数据并在按钮上方渲染
+		setTimeout(()=> {
+			this.add();
+		},2000)
+	},
+	methods: {
+		onsetFiles() {
+			let files1 = [{name: '测试文件名称.png'},{name: '测试文件名称2.png',}];
+			let files2 = new Map();
+			files2.set('测试文件名称.png',{name: '测试文件名称.png'});
+			this.$refs.lsjUploadRef.setFiles(files2)
+		},
+		add() {
+			this.data.push('在按钮上方动态添加了数据,此时需要调用组件的show刷新透明层top');
+			this.$nextTick(()=> {
+				uni.$emit('$upload-show',{});
+			})
+		},
+		// 移除某个文件
+		clear(name) {
+			// name=指定文件名,不传name默认移除所有文件
+			this.$refs.lsjUploadRef.clear(name);
+		},
+		// 某文件上传结束回调(成功失败都回调)
+		onuploadEnd(item,childId) {
+			console.log(`${item.name}已上传结束,上传状态=${item.type}`);
+			// 更新当前窗口状态变化的文件
+			this.files.set(item.name,item);
+			
+			// ---可删除--演示上传完成后取服务端数据
+			if (item['responseText']) {
+				console.log('演示服务器返回的字符串JSON转Object对象');
+				this.files.get(item.name).responseText = JSON.parse(item.responseText);
+			}
+			
+			// 微信小程序Map对象for循环不显示,所以转成普通数组,
+			// 如果你用不惯Map对象,也可以像这样转普通数组,组件使用Map主要是避免反复文件去重操作
+			// #ifdef MP-WEIXIN
+			this.wxFiles = [...this.files.values()];
+			// #endif
+			
+			// 强制更新视图
+			this.$forceUpdate();
+			
+			// ---可删除--演示判断是否所有文件均已上传成功
+			let isAll = [...this.files.values()].find(item=>item.type!=='success');
+			if (!isAll) {
+				console.log('已全部上传完毕');
+			}
+			else {
+				console.log(isAll.name+'待上传');
+			}
+			
+		},
+		// 上传进度回调
+		onprogress(item,childId) {
+			// 更新当前状态变化的文件
+			this.files.set(item.name,item);
+			
+			// 微信小程序Map对象for循环不显示,所以转成普通数组
+			// #ifdef MP-WEIXIN
+			this.wxFiles = [...this.files.values()];
+			// #endif
+			
+			// 强制更新视图
+			this.$forceUpdate();
+			
+		},
+		// 文件选择回调
+		onChange(files,childId) {
+			console.log('已选择的文件Map',JSON.stringify([...files]));
+			// 更新选择的文件 
+			this.files = files;
+			// 强制更新视图
+			this.$forceUpdate();
+			
+			// 微信小程序Map对象for循环不显示,所以转成普通数组,不要问为什么,我也不知道
+			// #ifdef MP-WEIXIN
+			this.wxFiles = [...this.files.values()]; 
+			// #endif
+			
+			// ---可删除--演示重新定位覆盖层控件
+			this.$nextTick(()=>{
+				console.log('演示重新定位 (提示:像示例里文件列表在按钮上方时就需要插入文件后更新webview位置)');
+				// 直接更新当前页面所有上传组件webview位置
+				uni.$emit('$upload-show',{});
+			});
+			
+		}
+	}
+}
+
+```
+
+## 温馨提示
+* 文件上传
+1. APP端请优先联调Android,上传成功后再运行iOS端,如iOS返回status=0则需要后端开启允许跨域;
+2. header的Content-Type类型需要与服务端要求一致,否则收不到附件(服务端若没有明文规定则“不传”,使用默认匹配)
+3. 服务端不清楚怎么配置跨域可加群咨询
+4. 欢迎加入QQ讨论群:
+	#### 交流群4:413918560
+	#### 交流群3:667530868
+	#### 交流群2:469580165
+	#### 交流群1:701468256
+