el-uploader的封装组件[vue2]

<template>
  <div class="custom-upload">
    <el-upload
      ref="uploadRef"
      v-bind="$attrs"
      :action="action"
      :headers="headers"
      :name="name"
      :data="data"
      :multiple="multiple"
      :drag="drag"
      :accept="accept"
      :limit="limit"
      :list-type="listType"
      :auto-upload="autoUpload"
      :http-request="httpRequest"
      :disabled="disabled"
      :show-file-list="showFileList"
      :file-list="fileList"
      :before-upload="handleBeforeUpload"
      :before-remove="handleBeforeRemove"
      :on-exceed="handleExceed"
      :on-change="handleChange"
      :on-success="handleSuccess"
      :on-error="handleError"
      :on-preview="handlePreview"
      :on-remove="handleRemove"
      :on-progress="handleProgress"
    >
      <!-- 默认插槽:自定义触发按钮 -->
      <template v-if="!$slots.default && !drag">
        <i class="el-icon-plus" v-if="listType == 'picture-card'"></i>
        <div style="text-align: left;" v-else>
          <el-button size="small" type="primary">{{ uploadText }}</el-button>
        </div>
        <p v-if="tip" class="el-upload__tip">{{ tip }}</p>
      </template>

      <!-- 拖拽模式默认区域 -->
      <template v-else-if="drag && !$slots.default">
        <div class="drag-area">
          <i class="el-icon-upload"></i>
          <div class="drag-text">{{ dragText }}</div>
          <div v-if="tip" class="el-upload__tip">{{ tip }}</div>
        </div>
      </template>

      <!-- 完全自定义触发区域 -->
      <slot v-else name="default" />

      <!-- 自定义文件列表(可选) -->
      <template v-if="$slots.file" #file="{ file }">
        <slot name="file" :file="file" />
      </template>
    </el-upload>

    <!-- 图片预览对话框(Element UI Dialog + Image 需单独处理) -->
    <el-dialog
      title="图片预览"
      :visible.sync="previewVisible"
      width="600px"
      append-to-body
      @close="handleClosePreview"
    >
      <img :src="previewUrl" style="width: 100%" alt="预览图片" />
    </el-dialog>
  </div>
</template>

<script>
import { uploadFile, downloadFile } from "@/api/file/index.js";
export default {
  name: "FileUploader",
  props: {
    // v-model:file-list 绑定
    value: {
      type: Array,
      default: () => []
    },
    //分类必填
    category: {
      type: String,
      default: "default",
      required: true
    },
    // 上传地址
    action: {
      type: String,
      default: "#"
    },
    headers: {
      type: Object,
      default: () => ({})
    },
    name: {
      type: String,
      default: "file"
    },
    data: {
      type: Object,
      default: () => ({})
    },
    multiple: Boolean,
    drag: Boolean,
    accept: String,
    limit: {
      type: Number,
      default: 5
    },
    maxSize: {
      type: Number,
      default: 5 // MB
    },
    listType: {
      type: String,
      default: "text",
      validator: val => ["text", "picture", "picture-card"].includes(val)
    },
    autoUpload: {
      type: Boolean,
      default: true
    },
    disabled: Boolean,
    showFileList: {
      type: Boolean,
      default: true
    },
    uploadText: {
      type: String,
      default: "点击上传"
    },
    dragText: {
      type: String,
      default: "将文件拖到此处,或点击上传"
    },
    tip: String,
    customBeforeUpload: Function,
    customBeforeRemove: Function
  },
  data() {
    return {
      fileList: this.value, // 内部文件列表
      previewVisible: false,
      previewUrl: ""
    };
  },
  watch: {
    value: {
      handler(newVal) {
        this.fileList = newVal;
      },
      immediate: true,
      deep: true
    },
    fileList: {
      handler(newVal) {
        this.$emit("input", newVal);
      },
      deep: true
    }
  },
  methods: {
    // 上传前校验
    handleBeforeUpload(file) {
      // 自定义校验钩子
      if (this.customBeforeUpload) {
        const result = this.customBeforeUpload(file);
        if (result === false) return false;
        if (result && result.then) {
          return result.catch(err => {
            this.$message.error(err.message || "上传校验失败");
            return false;
          });
        }
      }

      // 大小校验
      if (this.maxSize > 0) {
        const isOver = file.size / 1024 / 1024 > this.maxSize;
        if (isOver) {
          this.$message.error(
            `文件 ${file.name} 大小不能超过 ${this.maxSize} MB`
          );
          return false;
        }
      }

      // 类型校验(基于 accept 简单处理)
      if (this.accept) {
        const acceptTypes = this.accept.split(",").map(t => t.trim());
        const fileType = file.type;
        const fileExt =
          "." +
          file.name
            .split(".")
            .pop()
            .toLowerCase();
        const isValid = acceptTypes.some(type => {
          if (type === fileType) return true;
          if (type === fileExt) return true;
          if (
            type.endsWith("/*") &&
            fileType.startsWith(type.replace("/*", "/"))
          )
            return true;
          return false;
        });
        if (!isValid) {
          this.$message.error(
            `文件 ${file.name} 类型不支持,仅支持 ${this.accept}`
          );
          return false;
        }
      }

      return true;
    },

    // 移除前校验
    handleBeforeRemove(file, fileList) {
      if (this.customBeforeRemove) {
        return this.customBeforeRemove(file, fileList);
      }
      return true;
    },

    // 超出数量限制
    handleExceed(files, fileList) {
      this.$message.warning(
        `最多只能上传 ${this.limit} 个文件,请先移除多余文件`
      );
      this.$emit("exceed", files, fileList);
    },

    // 文件列表变化
    handleChange(file, fileList) {
      this.fileList = fileList;
      this.$emit("change", file, fileList);
    },

    // 上传成功
    handleSuccess(response, file, fileList) {
      setTimeout(() => {
        this.$message.success(`文件 ${file.name} 上传成功`);
      }, 0);
      this.$emit("success", response, file, fileList);
    },

    // 上传失败
    handleError(error, file, fileList) {
      this.$message.error(`文件 ${file.name} 上传失败`);
      this.$emit("error", error, file, fileList);
    },

    // 上传进度
    handleProgress(event, file, fileList) {
      this.$emit("progress", event, file, fileList);
    },

    // 移除文件
    handleRemove(file, fileList) {
      setTimeout(() => {
        this.$message.info(`已移除文件 ${file.name}`);
      }, 0);
      this.fileList = fileList;
      this.$emit("remove", file, fileList);
    },

    // 预览文件(增强图片预览)
    handlePreview(file) {
      this.$emit("preview", file);

      let url = file.response.url || file.url;
      if (!url && file.raw) {
        url = URL.createObjectURL(file.raw);
      }
      if (url) {
        this.$preview.open(url);
      } else {
        this.$message.info("该文件暂不支持预览");
      }
    },

    handleClosePreview() {
      if (this.previewUrl && this.previewUrl.startsWith("blob:")) {
        URL.revokeObjectURL(this.previewUrl);
      }
      this.previewUrl = "";
    },
    httpRequest({ file, onProgress, onSuccess, onError }) {
      const formData = new FormData();
      formData.append("category", this.category);
      formData.append("file", file);
      uploadFile(formData)
        .then(res => {
          if (res.code === 200) {
            onSuccess({
              ...res.result,
              url: `${process.env.API_ROOT}/api/v4/files/download?name=${res.result.objectName}`
            });
          } else {
            onError(new Error(res.message || "上传失败"));
          }
        })
        .catch(err => {
          onError(err);
        });
    },

    // 暴露方法供父组件调用
    submit() {
      this.$refs.uploadRef.submit();
    },
    clearFiles() {
      this.$refs.uploadRef.clearFiles();
    },
    abort(file) {
      this.$refs.uploadRef.abort(file);
    }
  }
};
</script>

<style scoped>
.custom-upload {
  width: 100%;
}
.drag-area {
  text-align: center;
  padding: 40px 20px;
  border: 1px dashed #dcdfe6;
  border-radius: 6px;
  background-color: #fafafa;
  transition: all 0.3s;
  cursor: pointer;
}
.drag-area:hover {
  border-color: #004fee;
}
.drag-area i {
  font-size: 48px;
  color: #909399;
  margin-bottom: 12px;
  display: inline-block;
}
.drag-text {
  font-size: 14px;
  color: #606266;
  margin-bottom: 8px;
}
.el-upload__tip {
  font-size: 12px;
  color: #909399;
  margin-top: 8px;
}
</style>
posted on 2026-04-17 17:49  jv_coder  阅读(4)  评论(0)    收藏  举报