Vue上传功能

该博客围绕Vue的文件上传功能展开,介绍了自动上传和手动上传两种方式。自动上传包括组件封装上传、多文件不同格式封装上传及单页面带请求接口上传;手动上传涉及上传文件限制中文的封装使用,借助pinyin-pro库实现中文转字母格式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Element官网

自动上传

auto-upload//是否在选取文件后立即进行上传 默认自动上传
自定义上传文件后缀类型与上传文件大小

action	--必选参数,上传的地址	
headers	--设置上传的请求头部	
multiple	--是否支持多选文件	
data	--上传时附带的额外参数	
name	--上传的文件字段名	
with-credentials	--支持发送 cookie 凭证信息	
show-file-list	--是否显示已上传文件列表	
drag	--是否启用拖拽上传	
accept	--接受上传的文件类型(thumbnail-mode 模式下此参数无效)
on-preview	--点击文件列表中已上传的文件时的钩子	function(file)	
on-remove	--文件列表移除文件时的钩子	function(file, fileList)
on-success	--文件上传成功时的钩子	function(response, file, fileList)
on-error	--文件上传失败时的钩子	function(err, file, fileList)
on-progress	--文件上传时的钩子	function(event, file, fileList)	
on-change	--文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用	
before-upload	--上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 
                 reject,则停止上传。	function(file)	
before-remove	--删除文件之前的钩子,参数为上传的文件和文件列表,若返回 false 或者返回 
                  Promise 且被 reject,则停止删除。	function(file, fileList)	—	—
list-type	--文件列表的类型	string	text/picture/picture-card	
auto-upload	--是否在选取文件后立即进行上传	
file-list	--上传的文件列表
http-request	--覆盖默认的上传行为,可以自定义上传的实现	
disabled	--是否禁用
limit	--最大允许上传个数	
on-exceed	--文件超出个数限制时的钩子	function(files, fileList)	

组件封装上传

components-FileUploader.vue

components文件夹下的components
如下所示:element-plus+vue3(ts)
文件格式 以txt格式为例:
具体格式可更替
文件上传地址为true 上传wav格式文件会报错显示请求调取服务地址上传错误
可把文件上传地址改为false 
如下面的 自动上传(去掉手动触发上传)里面功能是上传地址为false
<template>
  <el-upload
    ref="upload"
    :action="uploadUrl"
    :on-preview="handlePreview"
    :on-remove="handleRemove"
    :before-upload="beforeUpload"
    :on-success="handleSuccess"
    :on-error="handleError"
    :file-list="fileList"
    :auto-upload="autoUpload"
    :limit="limit"
    :on-exceed="handleExceed"
    :disabled="disabled"
    multiple>
    <el-button type="primary">上传</el-button>
    <template #tip>
      <div class="el-upload__tip">{{ tip }}</div>
    </template>
  </el-upload>
</template>

<script lang="ts">
import { defineComponent, ref, watch } from 'vue';
import { ElMessage } from 'element-plus'

export default defineComponent({
  name: 'FileUploader',
  props: {
    uploadUrl: { type: String, required: true },// 文件上传地址
    autoUpload: { type: Boolean, default: true }, // 是否自动上传
    fileList: { type: Array, default: () => [] },// 文件列表
    limit: { type: Number, default: 5 },// 文件数量限制
    disabled: { type: Boolean, default: false },// 禁用状态
    tip: { type: String, default: '只能上传 TXT 文件,且不超过 5MB' },// 提示信息
  },
  emits: ['uploaded', 'removed'],//组件同步上传成功/失败的状态
  setup(props, { emit }) {
    const upload = ref(null);
    // 文件预览
    const handlePreview = (file) => {
      console.log('Preview file:', file);
    };
    // 文件移除
    const handleRemove = (file, fileList) => {
      emit('removed', file, fileList);
    };
    /*文件上传前的钩子 (一般限制格式与大小)*/ 
    const beforeUpload = (file) => {
       // ['image/jpeg', 'image/png', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 
       //  'application/msword', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 
        //  'application/vnd.ms-excel'].includes(file.type);
      const isAcceptableType = ['text/plain'].includes(file.type);
      const isLt5M = file.size / 1024 / 1024 < 5;

      if (!isAcceptableType) {
        ElMessage.error('上传文件只能是 TXT 格式!');
      }
      if (!isLt5M) {
        ElMessage.error('上传文件大小不能超过 5MB!');
      }
      return isAcceptableType && isLt5M;
    };
   // 文件上传成功后的钩子
    const handleSuccess = (response, uploadFile, fileList) => {
      ElMessage.success('文件上传成功');
      emit('uploaded', response, uploadFile, fileList);
    };
   // 文件上传失败后的钩子
    const handleError = (error, uploadFile, fileList) => {
      ElMessage.error('文件上传失败');
      console.error('Upload error:', error);
    };
   // 文件超出限制时的钩子
    const handleExceed = (files, fileList) => {
      ElMessage.warning(
        `当前限制选择 ${props.limit} 个文件,本次选择了 ${
          files.length
        } 个文件,共选择了 ${files.length + fileList.length} 个文件`
      );
    };
   // 监听父组件传递的 fileList 变化
    watch(() => props.fileList, (newVal) => {
      if (upload.value) {
        upload.value.clearFiles();
        newVal.forEach(file => {
          // 假设 newVal 中的每个元素都有 name 和 type 属性
          const newFile = new File([], file.name, { type: file.type });
          upload.value!.handleAdd(newFile); // 使用 handleAdd 方法添加文件
        });
      }
    }, { deep: true });

    return {
      upload,
      handlePreview,
      handleRemove,
      beforeUpload,
      handleSuccess,
      handleError,
      handleExceed,
    };
  },
});
</script>

<style scoped>
.el-icon--upload {
  font-size: 67px;
  color: #c0c4cc;
  margin-bottom: 13px;
}

.el-upload__text em {
  color: #409eff;
}
</style>

页面使用

<template>
  <el-form :model="fileFrom" ref="formRef" :rules="rules" @submit.prevent>
    <el-form-item label="上传文件" prop="fileList" :label-width="formLabelWidth">
      <file-uploader 
        :upload-url="'https://jsonplaceholder.typicode.com/posts'"
        :auto-upload="true"
        :file-list="fileFrom.fileList"
        :limit="2"
        :disabled="false"
        @uploaded="handleUploaded"
        @removed="handleRemoved"
      />
    </el-form-item>
  </el-form>
</template>
<script lang='ts'>
import { reactive, ref, toRefs } from 'vue';
import { ElForm, ElMessage } from 'element-plus';
import FileUploader from './components/FileUploader.vue';

export default {
  name: '',
  components: {
    FileUploader,
  },
  setup() {
    const formRef = ref<InstanceType<typeof ElForm> | null>(null);
    const data = reactive({
      formLabelWidth: '140px',
      fileFrom: {
        fileList: [],
      }
    });

    const handleUploaded = (response, uploadFile, fileList) => {
      // 处理上传成功的文件
      console.log(response, uploadFile, fileList, 'o');
      // 更新文件列表或其他操作
    };

    const handleRemoved = (file, fileList) => {
      // 处理移除的文件
      console.log('File removed:', file, fileList);
      // 更新文件列表或其他操作
    };

    return {
      ...toRefs(data),
      handleUploaded,
      handleRemoved,
      formRef,
    };
  }
};
</script>

<style scoped>
/* 你的CSS样式 */
</style>

然后就可以处理你需要的逻辑使用

多文件不同格式封装上传

不请求服务 上传的地址作为入参定义
多个上传文件上传

components-FileUploader.vue

components文件夹下的components
如下所示:element-plus+vue3(ts)
<template>
  <el-upload
    ref="upload"
    :action="uploadUrl"
    :on-preview="handlePreview"
    :on-remove="handleRemove"
    :before-upload="beforeUpload"
    :auto-upload="autoUpload"
    :file-list="fileList"
    :limit="limit"
    :on-exceed="handleExceed"
    :disabled="disabled"
    :http-request="customUpload"
    multiple
  >
    <el-button type="primary">上传</el-button>
    <template #tip>
      <div v-if="currentTip === 'txt'" class="el-upload__tip">{{ tip }}</div>
      <div v-else-if="currentTip === 'xls'" class="el-upload__tip">{{ tip1 }}</div>
      <div v-else-if="currentTip === 'zip'" class="el-upload__tip">{{ tip2 }}</div>
    </template>
  </el-upload>
</template>

<script lang="ts">
import { defineComponent, ref, watch, computed  } from 'vue';
import { ElMessage } from 'element-plus';

export default defineComponent({
  name: 'FileUploader',
  props: {
    uploadUrl: {
      type: String,
      required: true,
    },
    autoUpload: {
      type: Boolean,
      default: true,
    },
    fileList: {
      type: Array,
      default: () => [],
    },
    limit: {
      type: Number,
      default: 5,
    },
    disabled: {
      type: Boolean,
      default: false,
    },
    // 提示信息
    tip: {
      type: String,
      default: '只能上传 TXT 文件,且不超过 5MB',
    },
    tip1: {
      type: String,
       default: "只能上传 XLS/XLSX 文件",
    },
    tip2: {
      type: String,
      default: "只能上传 ZIP/RAR/TAR 文件",
    },
    // 当前使用的提示信息类型
    currentTip: {
      type: String,
      default: 'tip'
    }
  },
  emits: ['uploaded', 'removed'],
  setup(props, { emit }) {
    const upload = ref(null);
  // 计算 accept 属性值
    const accept = computed(() => {
      if (props.currentTip === "xls") {
        return ".xls,.xlsx";
      } else if (props.currentTip === "zip") {
        return ".zip,.rar,.tar";
      } else if (props.currentTip === "txt") {
        return ".txt";
      }
      return "";
    });
    // 文件预览
    const handlePreview = (file) => {
      console.log('Preview file:', file);
    };

    // 文件移除
    const handleRemove = (file, fileList) => {
      emit('removed', file, fileList);
    };

    // 自定义上传方法
const customUpload = (options) => {
  if (props.uploadUrl === '#' || props.uploadUrl === '') {
    // 模拟上传成功的行为
    setTimeout(() => {
      const file = options.file;
      file.status = 'success';
      file.message = 'Mock upload success';
      emit('uploaded', file); // 直接传递文件对象
      ElMessage.success('文件上传成功');
    }, 1000); // 模拟延迟
  } else {
    emit('uploaded', options.file);
    // 如果有真实的上传 URL,则允许默认行为
    options.onSuccess();
    ElMessage.success('文件上传成功');
  }
};

     // 文件上传前的钩子
    const beforeUpload = (file) => {
    //如果上传包含中文 禁止上传 
      const isContainChinese = /[\u4e00-\u9fa5]/.test(file.name);
      if (isContainChinese) {
        ElMessage.error("文件名不能包含中文字符!");
        return false;
      }
      const fileExt = file.name.split(".").pop()?.toLowerCase();
      let acceptableExts: string[] = [];
      if (props.currentTip === "xls") {
        acceptableExts = ["xls", "xlsx"];
      } else if (props.currentTip === "zip") {
        acceptableExts = ["zip", "rar", "tar"];
      } else if (props.currentTip === "txt") {
        acceptableExts = ["txt"];
      }

      const isAcceptableType = acceptableExts.includes(fileExt as string);
      if (!isAcceptableType) {
        ElMessage.error(`上传文件只能是 ${props.currentTip} 格式!`);
        return false;
      }

      return true;
    };

    // 文件超出限制时的钩子
    const handleExceed = (files, fileList) => {
      ElMessage.warning(
        `当前限制选择 ${props.limit} 个文件,本次选择了 ${
          files.length
        } 个文件,共选择了 ${files.length + fileList.length} 个文件`
      );
    };

   // 监听父组件传递的 fileList 变化
watch(() => props.fileList, (newVal) => {
  if (upload.value) {
    upload.value.clearFiles();
    newVal.forEach(file => {
      upload.value.handleStart(file);
    });
  }
}, { deep: true });

    return {
      upload,
      handlePreview,
      handleRemove,
      beforeUpload,
      customUpload,
      handleExceed,
      accept,
    };
  },
});
</script>

<style scoped>
.el-icon--upload {
  font-size: 67px;
  color: #c0c4cc;
  margin-bottom: 13px;
}

.el-upload__text em {
  color: #409eff;
}
</style>

页面使用

<template>
  <!-- txt文件 -->
  <el-form-item label="txt文件" prop="txtFileList" :label-width="formLabelWidth">
    <file-uploader 
      :upload-url="'https://jsonplaceholder.typicode.com/posts'"
      :auto-upload="true"
      :file-list="fileFrom.txtFileList"
      :limit="1"
      :disabled="false"
      :currentTip="'txt'"
      @uploaded="handleTxtUploaded"
      @removed="handleRemoved"
    />
  </el-form-item>
  <!-- XLS/XLSX 文件 -->
  <el-form-item label="xls文件" prop="xlsFileList" :label-width="formLabelWidth">
    <file-uploader 
      :upload-url="'#'"
      :auto-upload="true"
      :file-list="fileFrom.xlsFileList"
      :limit="1"
      :disabled="false"
      :currentTip="'xls'"
      accept=".xls,.xlsx"
      @uploaded="handleXlsUploaded"
      @removed="handleRemoved"
    />
  </el-form-item>
  <!-- ZIP/RAR/TAR 文件 -->
  <el-form-item label="zip压缩文件" prop="zipFileList" :label-width="formLabelWidth">
    <file-uploader 
      :upload-url="'#'"
      :auto-upload="true"
      :file-list="fileFrom.zipFileList"
      :limit="1"
      :disabled="false"
      :currentTip="'zip'"
      accept=".zip,.rar,.tar"
      @uploaded="handleZipUploaded"
      @removed="handleRemoved"
    />
  </el-form-item>
   <el-button type="primary" @click="btnClik">点击</el-button>
</template>

<script lang='ts'>
import { reactive, ref, toRefs, watch,computed} from 'vue';
import { ElForm, ElMessage } from 'element-plus';
import FileUploader from '@/components/FileUploader.vue';

export default {
  name: '',
  components: {
    FileUploader,
  },
  emits: ['update:visible', 'addFrom', 'fileFrom'],
  setup(props, { emit }) {
    const formRef = ref<InstanceType<typeof ElForm> | null>(null);

    const data = reactive({
      formLabelWidth: '140px',
      fileFrom: {
        name: '',
        isOnline: false,
        txtFileList: [],
        xlsFileList: [],
        zipFileList: []
      }
    });

    const rules = reactive({
      name: [
        { required: true, message: '请输入名称', trigger: 'blur' },
        { min: 1, message: '长度至少为1个字符', trigger: 'blur' },
      ],
      txtFileList: [
        { required: true, message: '请上传txt文件', trigger: 'change' },
      ],
      xlsFileList: [
        { required: true, message: '请上传xls文件', trigger: 'change' },
      ],
      zipFileList: [
        { required: true, message: '请上传zip压缩文件', trigger: 'change' },
      ]
    });

    const handleTxtUploaded = (file) => {
      // 更新文件列表
      data.fileFrom.txtFileList = [file];
    };

    const handleXlsUploaded = (file) => {
      // 更新文件列表
      data.fileFrom.xlsFileList = [file];
    };

    const handleZipUploaded = (file) => {
      // 更新文件列表
      data.fileFrom.zipFileList = [file];
    };

    const handleRemoved = (file, fileList) => {
      console.log('File removed:', file, fileList);
      // 根据文件类型更新对应的文件列表
      if (data.fileFrom.txtFileList.includes(file)) {
        data.fileFrom.txtFileList = fileList;
      } else if (data.fileFrom.xlsFileList.includes(file)) {
        data.fileFrom.xlsFileList = fileList;
      } else if (data.fileFrom.zipFileList.includes(file)) {
        data.fileFrom.zipFileList = fileList;
      }
    };
    const btnClik = (file) => {
      console.log(data.fileFrom,'opop');
      
    }
    return {
      ...toRefs(data),
      handleTxtUploaded,
      handleXlsUploaded,
      handleZipUploaded,
      handleRemoved,
      rules,
      formRef,
      btnClik,
      // safeTxtFileList,
      // safeXlsFileList,
      // safeZipFileList
    };
  }
};
</script>

单页面带请求接口上传

渲染已请求文件在页面
element+vue2(js)
action为请求接口地址(必填)
headers如带token需把token单独放在请求头上
对应上传时间填写(可填)
accept//限制上传类型格式
on-preview点击文件对应列表(文件)
on-success文件上传成功
fileList对应上传文件的列表
before-upload上传之前的操作(限制提示上传格式)


:before-remove="()=>false"//禁止删除功能
 <el-upload
              :before-remove="()=>false"
                class="upload-demo"
                :action="action"
                :before-upload="beforeUpload"
                :data="fileData"
                :headers="headers"
                 accept=".doc,.xlsx,.pdf,.jpg,.png"
                :on-success="fileSuccessFunc"
                :on-preview="handlePreview"
                :file-list="fileList"
              >
                <el-button
                  size="mini"
                >点击上传</el-button>
                <div
                  slot="tip"
                  class="el-upload__tip"
                >只能上传doc/xlsx/pdf/jpg/png文件,且不超过2MB</div>
              </el-upload>
              
data(){
    return{
         action:"https//XXXXXXX(接口地址)",
         headers: {},
         fileDatas:[],//接收上传成功的数据
        fileList:[],//必填(用来存储上传后的文件列表)
    }
}
 beforeUpload(file) {//上传之前的限制文件格式
      this.headers.后端规定token名称 = 'XXXXXXXXXXXXXX';//token(用来放token)
      this.headers.time = new Date().getTime;//可选
      const fileSuffix = file.name.substring(file.name.lastIndexOf(".") + 1);
      const whiteList = ["doc", "xlsx", "pdf", "jpg", "png"];
      if (whiteList.indexOf(fileSuffix) === -1) {
        this.$message({
          message: '上传文件只能是doc(Doc), xlsx(Xlsx), pdf(Pdf), jpg(Jpg), png(Png)格式',
          type: 'warning'
        });
        return false;
      }
      const isLt2M = file.size / 1024 / 1024 < 2;
      if (!isLt2M) {
        this.$message({
          message: '上传文件大小不能超过2MB',
          type: 'warning'
        });
        return false;
      }
    },
    // 上传成功
    fileSuccessFunc(response, file, fileList) {
    //实现多个数据放在数组中
    var arr=[]
    arr=[...arr,respose.data.url];
     this.fileDatas=arr//上传成功的数据
    },
     handlePreview(file) {
     //这个是上传后定位对应单个文件
    //单个文件列表值
     var that = this;
      var a = document.createElement('a');
      var event = new MouseEvent('click');
      a.download = file.name;
      a.href = file.url;
      a.dispatchEvent(event);
     console.log(file);//单个文件的值
  }

PS

one.jpg,two.pdf
把对应的后缀去掉只要前面数值怎么搞呢?
如下所示
    var a="a.jpg";
    var b=a.split(".")[0];
    console.log(b);//a

参考有的小米华为自带浏览器打不开图片解决

手动上传

上传文件限制中文(封装使用)

上传文件为手动上传 不去请求服务
如下所示:element-plus+vue3(ts)
首先上传文件不能上传带中文的名称 根据pinyin-pro库转写中文变成字母 达到想要的结果
下面是封装的方法-如下所示:

utils-pinyin.js

需要安装pinyin-pro库
如下链接

pinyin-pro安装使用

pinyin-pro库封装中文转字母格式功能

import { pinyin } from 'pinyin-pro';
/* 提取上传文件带中文名称转成字母格式方法
   并且去掉特殊字符
*/ 
export function convertFileNameToPinyin(file: File): File {
    // 提取文件扩展名
    const extension = file.name.split('.').pop();
    if (!extension) {
      console.error('File has no extension:', file.name);
      return file; // 如果没有扩展名,直接返回原文件对象
    }
  
    // 去掉文件扩展名后的名字部分
    const nameWithoutExtension = file.name.replace(new RegExp(`.${extension}$`), '');
    console.log('Original name without extension:', nameWithoutExtension);
  
    // 将名字部分转换为拼音
    let pinyinName = '';
    try {
      pinyinName = pinyin(nameWithoutExtension, { toneType: 'none' }).toLowerCase().replace(/\s+/g, '-');
      console.log('Converted to pinyin:', pinyinName);
    } catch (error) {
      console.error('Error converting filename to pinyin:', error);
      pinyinName = nameWithoutExtension; // 如果转换失败,则保留原名
    }
  
    // 定义要移除的特殊字符列表
    const specialCharsToRemove = [
      '\\', '/', ':', '*','(', ')', '?', '"', '<', '>', '|', '$', '+', '{', '}', '[', ']', '(', ')'
    ];
  
    // 移除所有特殊字符,包括括号和其他非法字符
    let cleanedPinyinName = pinyinName;
    specialCharsToRemove.forEach(char => {
      cleanedPinyinName = cleanedPinyinName.split(char).join('');
    });
    console.log('Cleaned pinyin name:', cleanedPinyinName);
  
    // 创建新的带有拼音文件名的Blob对象
    const newFile = new File([file], `${cleanedPinyinName}.${extension}`, { type: file.type });
    console.log('New file object:', newFile);
  
    return newFile;
  }

1 手动触发上传

封装上传的组件

before-upload设置手动上传 不去请求服务
手动触发上传
需求:
增添的时候只需要带传入文件链接的入参 不去请求服务

components-FileUploader.vue

<template>
  <div>
    <el-upload
      ref="upload"
      :action="uploadUrl"
      :on-preview="handlePreview"
      :on-remove="handleRemove"
      :before-upload="beforeUpload"
      :auto-upload="false"
      :limit="limit"
      :disabled="disabled"
      multiple
      :accept="'.wav'"
      :file-list="fileList"
      :on-exceed="handleExceed"
      :http-request="customUpload"
    >
      <el-button type="primary">选择文件</el-button>
      <template #tip>
        <div style="display: flex">
          <div class="el-upload__tip">{{ tip }}</div>
          <el-button size="small" style="margin: 11px 6px;" @click="submitUpload" type="primary">
            <el-icon><Check /></el-icon>
          </el-button>
        </div>
      </template>
    </el-upload>

    <!-- <el-button  @click="submitUpload" style="margin-top: 20px;">保存</el-button> -->
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, watch } from "vue";
import { ElMessage } from "element-plus";
import { convertFileNameToPinyin } from "../utils/pinyin";
import { Edit, Search, Check, CirclePlus } from "@element-plus/icons-vue";

export default defineComponent({
  name: "FileUploader",
  components: {
    Edit,
    Search,
    Check, // 注册 Plus 图标组件
    CirclePlus, // 注册 CirclePlus 图标组件
  },
  props: {
    uploadUrl: { type: String, required: true },
    autoUpload: { type: Boolean, default: false },
    fileList: { type: Array, default: () => [] },
    limit: { type: Number, default: 5 },
    disabled: { type: Boolean, default: false },
    tip: {
      type: String,
      default: "只能上传音视频文件(wav格式),且不超过 50MB",
    },
  },
  emits: ["uploaded", "removed", "Preview"],
  setup(props, { emit }) {
    const upload = ref(null);

    // 模拟上传的方法
    const customUpload = (options) => {
      const file = options.file;
      // 在模拟上传前转换文件名
      const convertedFile = convertFileNameToPinyin(file);

      setTimeout(() => {
        const response = { success: true, message: "模拟上传" };
        emit("uploaded", response, convertedFile); // 使用转换后的文件对象
        ElMessage.success("文件保存成功");
      }, 1000);
    };

    // 手动触发上传
    const submitUpload = () => {
      if (upload.value) {
        upload.value.submit(); // 触发所有选中的文件上传
      }
    };

    // 假设这是你在 FileUploader.vue 中处理预览的方法
    const handlePreview = (file) => {
      // 如果 file 是一个有效的 File 对象,则发射 Preview 事件
      if (file.raw && file.raw instanceof File) {
        emit("Preview", file.raw); // 发射原始文件对象
      } else {
        //证明是编辑的状态(接口返回 发射file)
        emit("Preview", file);
        console.warn("Invalid file object for preview:", file);
      }
    };

    const handleRemove = (file, fileList) => {
      emit("removed", file, fileList);
    };

    const beforeUpload = (file) => {
      //自动上传才会生效(当前手动上传)
      const isAcceptableType = [
        //限制wav格式
        "audio/mpeg",
        "audio/wav",
        "audio/ogg",
        "video/mp4",
        "video/webm",
        "video/quicktime",
      ].includes(file.type);
      const isLt50M = file.size / 1024 / 1024 < 50;

      if (!isAcceptableType) {
        ElMessage.error("上传文件只能是音视频格式!");
      }
      if (!isLt50M) {
        ElMessage.error("上传文件大小不能超过 50MB!");
      }
      return isAcceptableType && isLt50M;
    };

    const handleExceed = (files, fileList) => {
      ElMessage.warning(
        `当前限制选择 ${props.limit} 个文件,本次选择了 ${
          files.length
        } 个文件,共选择了 ${files.length + fileList.length} 个文件`
      );
    };

    watch(
      () => props.fileList,
      (newVal) => {
        if (Array.isArray(newVal)) {
          if (upload.value) {
            upload.value.clearFiles();
            newVal.forEach((file) => {
              // el-upload 并没有直接的 handleStart 方法来模拟文件选择。
              // 需要预览文件或显示已选文件,需要手动创建 File 对象或使用其他方式。
            });
          }
        } else {
          console.warn("fileList prop is not an array:", newVal);
        }
      },
      { deep: true, immediate: true }
    ); // 使用 immediate 确保初始化时也触发

    return {
      upload,
      customUpload,
      handlePreview,
      handleRemove,
      beforeUpload,
      handleExceed,
      submitUpload, // 提交上传函数
    };
  },
});
</script>

<style lang="scss" scoped>
// :deep(.el-upload) {
//   display: flex !important;
//   justify-content: space-between !important;
// }
</style>

页面使用

<template>
  <el-form :model="fileFrom" ref="formRef" :rules="rules" @submit.prevent>
    <el-form-item label="上传文件" prop="fileList" :label-width="formLabelWidth">
      <file-uploader
          ref="fileUploader"
          :upload-url="'#'"
          :auto-upload="false"
          :file-list="formData.fileList"
          :limit="1"
          :disabled="false"
          @Preview="handPreview"
          @uploaded="handleUploaded"
          @removed="handleRemoved"
        />
    </el-form-item>
  </el-form>
</template>
<script lang='ts'>
import { reactive, ref, toRefs } from 'vue';
import { ElForm, ElMessage } from 'element-plus';
import FileUploader from './components/FileUploader.vue';

export default {
  name: '',
  components: {
    FileUploader,
  },
  setup() {
    const formRef = ref<InstanceType<typeof ElForm> | null>(null);
    const data = reactive({
      formLabelWidth: '140px',
      formData: {
        fileList: [],
      }
    });
    const handPreview = async (file: File) => {
      // 预览
      console.log(file);
      
    }
    const handleUploaded = (response, uploadFile, fileList) => {
      // 处理上传成功的文件
      console.log(response, uploadFile, fileList, 'o');
      // 更新文件列表或其他操作
    };
    const handleRemoved = (file, fileList) => {
      // 处理移除的文件
      console.log('File removed:', file, fileList);
      // 更新文件列表或其他操作
    };

    return {
      ...toRefs(data),
      handleUploaded,
      handleRemoved,
      formRef,
      handPreview,
    };
  }
};
</script>

<style scoped>
/* 你的CSS样式 */
</style>

2 自动上传(去掉手动触发上传)

封装上传的组件

components-FileUploader.vue

限制wav格式文件格式上传
不请求服务 前端上传 只获取上传文件对应的参数
<template>
  <div>
    <el-upload
      ref="upload"
      :auto-upload="true"
      :action="uploadUrl"
      :on-preview="handlePreview"
      :on-remove="handleRemove" 
      :limit="limit"
      :disabled="disabled"
      multiple
      :accept="'.wav'"
      :file-list="fileList"
      :before-upload="beforeUpload"
      :http-request="customUpload"
      :on-exceed="handleExceed"
    >
      <el-button type="primary">选择文件</el-button>
      <template #tip>
        <div class="el-upload__tip">{{ tip }}</div>
      </template>
    </el-upload>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, watch } from "vue";
import { ElMessage } from "element-plus";
import { Check } from "@element-plus/icons-vue";

export default defineComponent({
  name: "FileUploader",
  components: {
    Check,
  },
  props: {
    uploadUrl: { type: String, required: false }, // 不再需要实际的上传 URL
    autoUpload: { type: Boolean, default: true }, // 默认启用自动上传
    fileList: { type: Array, default: () => [] },
    limit: { type: Number, default: 5 },
    disabled: { type: Boolean, default: false },
    tip: {
      type: String,
      default: "只能上传音视频文件(wav格式),且不超过 50MB",
    },
  },
  emits: ["uploaded", "removed", "preview"],
  setup(props, { emit }) {
    const upload = ref(null);

    // 自定义上传方法(纯前端处理)
    const customUpload = async (options) => {
      const file = options.file;

      // 可以在这里对文件进行前端处理,例如读取文件内容
      const reader = new FileReader();
      reader.onload = function(e) {
        console.log("File content:", e.target?.result); // 文件内容
        emit("uploaded", { success: true, message: "文件上传成功" }, file);
        ElMessage.success("文件上传成功");
      };
      reader.readAsArrayBuffer(file); // 或者使用 readAsDataURL, readAsText 等

      // 如果只是想获取文件的基本信息,则可以直接使用 file 对象
      console.log("File info:", file);
    };

    // 假设这是你在 FileUploader.vue 中处理预览的方法
    const handlePreview = (file) => {
      if (file.raw && file.raw instanceof File) {
        emit("preview", file.raw); // 发射原始文件对象
      } else {
        emit("preview", file);
        console.warn("Invalid file object for preview:", file);
      }
    };

    const handleRemove = (file, fileList) => {
      emit("removed", file, fileList);
    };

    const beforeUpload = (file) => {
      // 检查文件名是否包含中文字符
      const hasChineseChars = /[\u4e00-\u9fa5]/.test(file.name);
      if (hasChineseChars) {
        ElMessage.error("文件名不能包含中文字符");
        return false;
      }

      // 限制 wav 格式
      const isAcceptableType = ["audio/wav"].includes(file.type);
      const isLt50M = file.size / 1024 / 1024 < 50;

      if (!isAcceptableType) {
        ElMessage.error("上传文件只能是音频格式!");
      }
      if (!isLt50M) {
        ElMessage.error("上传文件大小不能超过 50MB!");
      }
      return isAcceptableType && isLt50M;
    };

    const handleExceed = (files, fileList) => {
      ElMessage.warning(
        `当前限制选择 ${props.limit} 个文件,本次选择了 ${
          files.length
        } 个文件,共选择了 ${files.length + fileList.length} 个文件`
      );
    };

    watch(
      () => props.fileList,
      (newVal) => {
        if (Array.isArray(newVal)) {
          if (upload.value) {
            upload.value.clearFiles();
            newVal.forEach((file) => {
              // el-upload 并没有直接的 handleStart 方法来模拟文件选择。
              // 需要预览文件或显示已选文件,需要手动创建 File 对象或使用其他方式。
            });
          }
        } else {
          console.warn("fileList prop is not an array:", newVal);
        }
      },
      { deep: true, immediate: true }
    ); // 使用 immediate 确保初始化时也触发

    return {
      upload,
      customUpload,
      handlePreview,
      handleRemove,
      beforeUpload,
      handleExceed,
    };
  },
});
</script>

<style lang="scss" scoped>
// 样式保持不变
</style>

页面使用

<template>
  <el-form :model="fileFrom" ref="formRef" :rules="rules" @submit.prevent>
    <el-form-item label="上传文件" prop="fileList" :label-width="formLabelWidth">
      <file-uploader
          ref="fileUploader"
          :upload-url="'#'"
          :auto-upload="false"
          :file-list="formData.fileList"
          :limit="1"
          :disabled="false"
          @Preview="handPreview"
          @uploaded="handleUploaded"
          @removed="handleRemoved"
        />
    </el-form-item>
  </el-form>
</template>
<script lang='ts'>
import { reactive, ref, toRefs } from 'vue';
import { ElForm, ElMessage } from 'element-plus';
import FileUploader from './components/FileUploader.vue';

export default {
  name: '',
  components: {
    FileUploader,
  },
  setup() {
    const formRef = ref<InstanceType<typeof ElForm> | null>(null);
    const data = reactive({
      formLabelWidth: '140px',
      formData: {
        fileList: [],
      }
    });
    const handPreview = async (file: File) => {
      // 预览
      console.log(file);
      
    }
    const handleUploaded = (response, uploadFile, fileList) => {
      // 处理上传成功的文件
      console.log(response, uploadFile, fileList, 'o');
      // 更新文件列表或其他操作
    };
    const handleRemoved = (file, fileList) => {
      // 处理移除的文件
      console.log('File removed:', file, fileList);
      // 更新文件列表或其他操作
    };

    return {
      ...toRefs(data),
      handleUploaded,
      handleRemoved,
      formRef,
      handPreview,
    };
  }
};
</script>

<style scoped>
/* 你的CSS样式 */
</style>

----

编辑模块 回显文件上传/上传文件

在这里插入图片描述

上传文件的入参为formData
1 编辑时有对应的文件 如果不上传保存 默认保存的回显文件 
(这种情况需要blob把文件下载然后再次作入参 否则会显示[object,object])
2 上传文件

封装prepareFormData 函数

  • utils-formDataUtils.ts
// 封装文件处理函数,接收一个对象参数,包含其他字段和文件列表
export const prepareFormData = async (data: { [key: string]: any; fileList?: any[] }) => {
    const formData = new FormData();
    // 遍历对象,将除 fileList 外的其他字段添加到 FormData 中 (fileLists是上传文件的列表数据)
    for (const key in data) {
        if (key!== 'fileList') {
            formData.append(key, data[key].toString());
        }
    }

    const filePromises: Promise<void>[] = [];
    const fileList = data.fileList || [];

    if (Array.isArray(fileList)) {
        fileList.forEach((fileItem: any) => {
            if (fileItem instanceof File) {
                formData.append('vpFile', fileItem);
            } else if (fileItem.url && typeof fileItem === 'object') {
                const fetchFile = async () => {
                    try {
                        const response = await fetch(fileItem.url);
                        if (!response.ok) {
                            throw new Error(`Failed to fetch file: ${response.status} ${response.statusText}`);
                        }
                        const blob = await response.blob();
                        const fileName = fileItem.name || 'unknown';
                        const file = new File([blob], fileName, { type: blob.type });
                        console.log('Downloaded and converted to File:', file);
                        formData.append('vpFile', file);//VpFile是文件的入参(具体根据后端的定义更替)
                    } catch (error) {
                        console.error('Error fetching file:', error);
                        // 如果下载失败,回退到 URL
                        formData.append('vpFile', fileItem.url); 
                    }
                };
                filePromises.push(fetchFile());
            }
        });
    }

    // 等待所有文件处理完成
    await Promise.all(filePromises);

    return formData;
};

页面编辑 保存

// 导入封装好的函数
import { prepareFormData } from "@/utils/formDataUtils"; 

const vprSave = async (e: any) => {//编辑时保存按钮
    try {
        const formData = await prepareFormData({//对应的入参(根据对应接口入参更替)
            name: e.name,
            idNumber: e.idNumber,
            areaId: e.areaId,
            fileList: e.fileList
        });

        const response = await proAdd(formData);//对应的请求封装地址
        await nextTick();
        getPrVp();//初始化表格的数据 
        data.dialog.visible = false; // 弹框关闭
           } catch (error) {
        console.error('Error creating:', error);
    }
};

分片上传

又称为断点续传通常用于上传大文件或在网络条件不稳定的情况下,
确保文件能够完整、可靠地上传到服务器
带上传进度展示百分百效果

封装方法 多页面调用

npm install axios mockjs spark-md5 --save

该结合mock模拟接口

请添加图片描述

mock.ts

import Mock from 'mockjs';

// 模拟分片上传接口
Mock.mock('/api/upload-chunk', 'post', (options) => {
  console.log('接收到分片:', options.body);
  return {
    success: true,
    message: '分片上传成功',
  };
});

// 模拟合并分片接口
Mock.mock('/api/merge-chunks', 'post', (options) => {
  console.log('开始合并分片:', options.body);
  return {
    success: true,
    message: '分片合并成功',
  };
});

uploadService.ts

import axios from 'axios';
import SparkMD5 from 'spark-md5';

class UploadService {
  chunkSize: number;

  constructor() {
    this.chunkSize = 1 * 1024 * 1024; // 每个分片大小为 1MB
  }

  /**
   * 计算文件的 MD5 值
   * @param {File} file - 文件对象
   * @returns {Promise<string>} - MD5 值
   */
  calculateMD5(file: File): Promise<string> {
    return new Promise((resolve, reject) => {
      const blobSlice =
        File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
      const chunkSize = 2 * 1024 * 1024; // 每次读取 2MB
      const chunks = Math.ceil(file.size / chunkSize);
      let currentChunk = 0;
      const spark = new SparkMD5.ArrayBuffer();
      const fileReader = new FileReader();

      fileReader.onload = (e: ProgressEvent<FileReader>) => {
        if (e.target?.result) {
          spark.append(e.target.result as ArrayBuffer); // 添加数组缓冲区
        }
        currentChunk++;

        if (currentChunk < chunks) {
          this.loadNextChunk(fileReader, file, blobSlice, currentChunk, chunkSize);
        } else {
          resolve(spark.end());
        }
      };

      fileReader.onerror = () => {
        reject(new Error('读取文件时出错'));
      };

      this.loadNextChunk(fileReader, file, blobSlice, currentChunk, chunkSize);
    });
  }

  /**
   * 加载下一个分片
   * @param {FileReader} fileReader - 文件读取器
   * @param {File} file - 文件对象
   * @param {Function} blobSlice - 切片函数
   * @param {number} currentChunk - 当前分片索引
   * @param {number} chunkSize - 分片大小
   */
  loadNextChunk(
    fileReader: FileReader,
    file: File,
    blobSlice: (this: File, start: number, end: number) => Blob,
    currentChunk: number,
    chunkSize: number
  ): void {
    const start = currentChunk * chunkSize;
    const end = Math.min(file.size, start + chunkSize);
    fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
  }

  /**
   * 动态上传单个分片
   * @param {Blob} chunk - 分片数据
   * @param {string} fileName - 文件名
   * @param {string} md5 - 文件 MD5 值
   * @param {number} index - 分片索引
   * @param {number} totalChunks - 总分片数
   * @param {string} uploadUrl - 上传地址
   */
  async uploadChunk(chunk: Blob, fileName: string, md5: string, index: number, totalChunks: number, uploadUrl: string): Promise<void> {
    const formData = new FormData();
    formData.append('file', chunk);
    formData.append('fileName', fileName);
    formData.append('md5', md5);
    formData.append('index', index.toString());
    formData.append('totalChunks', totalChunks.toString());

    await axios.post(uploadUrl, formData, {
      headers: { 'Content-Type': 'multipart/form-data' },
    });
  }

  /**
   * 合并分片
   * @param {string} fileName - 文件名
   * @param {string} md5 - 文件 MD5 值
   * @param {number} totalChunks - 总分片数
   * @param {string} mergeUrl - 合并地址
   */
  async mergeChunks(fileName: string, md5: string, totalChunks: number, mergeUrl: string): Promise<void> {
    await axios.post(mergeUrl, { fileName, md5, totalChunks });
  }

  /**
   * 上传多个文件
   * @param {File[]} files - 文件列表
   * @param {Function} onProgress - 进度回调
   * @param {string} uploadUrl - 上传地址
   * @param {string} mergeUrl - 合并地址
   */
  async uploadFiles(files: File[], onProgress: (fileName: string, progress: number) => void, uploadUrl: string, mergeUrl: string): Promise<void> {
    for (const file of files) {
      await this.uploadFile(file, onProgress, uploadUrl, mergeUrl);
    }
  }

  /**
   * 上传单个文件
   * @param {File} file - 文件对象
   * @param {Function} onProgress - 进度回调
   * @param {string} uploadUrl - 上传地址
   * @param {string} mergeUrl - 合并地址
   */
  async uploadFile(file: File, onProgress: (fileName: string, progress: number) => void, uploadUrl: string, mergeUrl: string): Promise<void> {
    const md5 = await this.calculateMD5(file);
    const totalChunks = Math.ceil(file.size / this.chunkSize);

    for (let i = 0; i < totalChunks; i++) {
      const start = i * this.chunkSize;
      const end = Math.min(file.size, start + this.chunkSize);
      const chunk = file.slice(start, end);

      try {
        await this.uploadChunk(chunk, file.name, md5, i, totalChunks, uploadUrl);
        const progress = Math.round(((i + 1) / totalChunks) * 100);
        onProgress(file.name, progress); // 更新进度
      } catch (error) {
        throw new Error(`分片上传失败: ${error.message}`);
      }
    }

    // 合并分片
    await this.mergeChunks(file.name, md5, totalChunks, mergeUrl);
  }
}

export default new UploadService();

页面使用

<template>
  <div class="upload-container">
    <h3>分片上传示例</h3>
    <input type="file" multiple @change="handleFileChange" />
    <button @click="startUpload" :disabled="isUploading">开始上传</button>
    <div v-for="(progress, index) in progresses" :key="index">
      <p>{{ progress.name }}: {{ progress.value }}%</p>
      <progress :value="progress.value" max="100"></progress>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import '../src/mock/mock'
import uploadService from '../src/utils/uploadService';

// 文件列表
const files = ref([]);
// 是否正在上传
const isUploading = ref(false);
// 每个文件的上传进度
const progresses = ref([]);

// 监听文件选择事件
const handleFileChange = (event) => {
  files.value = Array.from(event.target.files);
};

// 开始上传
const startUpload = async () => {
  if (!files.value.length) {
    alert('请选择文件');
    return;
  }

  isUploading.value = true;
  progresses.value = files.value.map(file => ({ name: file.name, value: 0 }));

  try {
    await uploadService.uploadFiles(
      files.value,
      (fileName, progress) => {
        const fileProgress = progresses.value.find(p => p.name === fileName);
        if (fileProgress) {
          fileProgress.value = progress;
        }
      },
      '/api/upload-chunk', // 动态设置上传地址
      '/api/merge-chunks'  // 动态设置合并地址
    );

    alert('所有文件上传成功!');
  } catch (error) {
    console.error('上传失败:', error.message);
    alert('上传失败,请稍后重试');
  }

  isUploading.value = false;
};
</script>

<style scoped>
/* 样式代码保持不变 */
</style>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值