基于element-puls二次封装的图片上传组件

基于element-puls二次封装的图片上传组件

基于Element Plus的Upload组件封装的图片上传组件,提供更简单易用的API。

功能特点

  • 支持单图/多图上传
  • 支持拖拽上传
  • 支持自定义上传前验证
  • 支持自定义上传成功后回调
  • 支持自定义上传数量限制
  • 支持自定义上传区域尺寸
  • 支持自定义上传区域图标
  • 支持自定义上传请求头
  • 支持自定义上传接口
  • 支持v-model双向绑定

基本使用

<template>
  <ReImageUpload v-model="imageUrl" />
</template>

<script lang="ts" setup>
import { ref } from 'vue';
import { ReImageUpload } from '@/components/ReImageUpload';

const imageUrl = ref('');
</script>

组件参数

参数名类型默认值说明
modelValuestring | string[]''绑定值,单图时为字符串,多图时为字符串数组
dragbooleanfalse是否启用拖拽上传
limitnumber1上传数量限制
widthstring | number'178px'上传区域宽度
heightstring | number'178px'上传区域高度
iconstring''自定义图标URL
actionstring/file/image上传接口URL
headersRecord<string, string>{}自定义请求头
maxSizenumber2上传文件大小限制(MB)
showFileListbooleanfalse是否显示文件列表
acceptstring'image/jpg,image/jpeg,image/png,image/gif'文件类型限制
tipstring'点击上传图片'提示文字
disabledbooleanfalse是否禁用

事件

事件名参数说明
update:modelValuestring | string[]更新绑定值
before-upload(file: File) => boolean上传前钩子,返回false则阻止上传
on-success(response: any, file: File, fileList: File[]) => void上传成功回调
on-error(error: any, file: File, fileList: File[]) => void上传失败回调
on-exceed(files: File[], fileList: File[]) => void超出数量限制回调

使用示例

基础用法

<ReImageUpload v-model="imageUrl" />

拖拽上传

<ReImageUpload 
  v-model="imageUrl" 
  drag 
  width="300px"
  height="200px"
  tip="拖拽图片到此处上传"
/>

多图上传

<ReImageUpload 
  v-model="imageUrls" 
  :limit="3"
  @on-exceed="handleExceed"
/>

自定义验证和回调

<ReImageUpload 
  v-model="imageUrl" 
  @before-upload="beforeUploadHandler"
  @on-success="onSuccessHandler"
/>

<script setup>
const beforeUploadHandler = (file) => {
  const isJPG = file.type === 'image/jpeg';
  if (!isJPG) {
    ElMessage.error('上传图片只能是JPG格式!');
    return false;
  }
  return true;
};

const onSuccessHandler = (response, file, fileList) => {
  console.log('自定义上传成功回调', response);
  ElMessage.success('图片上传成功!');
};
</script>

自定义尺寸和图标

<ReImageUpload 
  v-model="imageUrl" 
  width="120px"
  height="120px"
  icon="/path/to/custom-icon.png"
  tip="点击上传自定义图标"
/>

自定义接口和请求头

<ReImageUpload 
  v-model="imageUrl" 
  :action="customAction"
  :headers="customHeaders"
  tip="使用自定义上传接口"
/>

<script setup>
import { baseUrlApi } from '@/api/utils';

const customAction = baseUrlApi('/file/imageCustom');
const customHeaders = {
  'X-Custom-Header': '自定义请求头'
};
</script>

代码

<script lang="ts" setup>
/**
 * ReImageUpload 图片上传组件
 * 基于element-plus的upload组件进行封装
 * 支持自定义上传前验证、上传后回调、数量限制、尺寸设置等
 */
import {
  defineProps,
  withDefaults,
  defineEmits,
  ref,
  computed,
  watch,
  onMounted
} from "vue";
import { ElMessage } from "element-plus";
import { Plus, Delete, UploadFilled } from "@element-plus/icons-vue";
import { formatToken, getToken } from "@/utils/auth";
import { baseUrlApi, baseUploadApi } from "@/api/utils";

// 文件信息接口
interface FileItem {
  name: string;
  path: string;
}

const props = withDefaults(
  defineProps<{
    /** 图片URL或文件数组 */
    modelValue?: string | FileItem[];
    /** 是否使用拖拽上传 */
    drag?: boolean;
    /** 上传最大数量 */
    limit?: number;
    /** 上传区域宽度 */
    width?: string | number;
    /** 上传区域高度 */
    height?: string | number;
    /** 自定义图标 */
    icon?: string;
    /** 自定义接口URL */
    action?: string;
    /** 自定义上传请求头 */
    headers?: Record<string, string>;
    /** 上传文件大小限制(MB) */
    maxSize?: number;
    /** 是否显示文件列表 */
    showFileList?: boolean;
    /** 文件类型限制 */
    accept?: string;
    /** 提示文字 */
    tip?: string;
    /** 是否禁用 */
    disabled?: boolean;
  }>(),
  {
    modelValue: "",
    drag: false,
    limit: 1,
    width: "178",
    height: "178",
    icon: "",
    action: baseUrlApi("/file/image"),
    headers: () => ({}),
    maxSize: 2,
    showFileList: false,
    accept: "image/jpg,image/jpeg,image/png,image/gif",
    tip: "点击上传图片",
    disabled: false
  }
);

const emit = defineEmits<{
  (e: "update:modelValue", value: string | FileItem[]): void;
  (e: "before-upload", file: File): boolean;
  (e: "on-success", response: any, file: File, fileList: File[]): void;
  (e: "on-error", error: any, file: File, fileList: File[]): void;
  (e: "on-exceed", files: File[], fileList: File[]): void;
}>();

const uploadRef = ref();
const isMultiple = computed(
  () => Array.isArray(props.modelValue) || props.limit > 1
);
const fileList = ref<any[]>([]);

// 监听modelValue变化,更新fileList
watch(
  () => props.modelValue,
  newVal => {
    updateFileList(newVal);
  },
  { immediate: true, deep: true }
);

onMounted(() => {
  updateFileList(props.modelValue);
});

/**
 * 更新文件列表
 */
function updateFileList(value) {
  if (!value) {
    fileList.value = [];
    return;
  }

  if (Array.isArray(value)) {
    // 多图模式
    fileList.value = value.map((item, index) => {
      if (typeof item === "string") {
        return {
          name: `图片${index + 1}`,
          url: item
        };
      } else {
        return {
          name: item.name || `图片${index + 1}`,
          url: item.path
        };
      }
    });
  } else if (typeof value === "string" && value) {
    // 单图模式
    fileList.value = [
      {
        name: "图片",
        url: value
      }
    ];
  } else {
    fileList.value = [];
  }
}

/**
 * 获取请求头信息
 */
const getHeaders = () => {
  // 默认带上token
  const defaultHeaders = {
    Authorization: formatToken(getToken().token)
  };
  // 合并自定义请求头
  return {
    ...defaultHeaders,
    ...props.headers
  };
};

/**
 * 上传前验证
 * @param file 文件对象
 */
const handleBeforeUpload = (file: File) => {
  // 检查文件类型
  const isValidType = file.type.indexOf("image/") !== -1;
  if (!isValidType) {
    ElMessage.error("上传图片只能是图片格式!");
    return false;
  }

  // 检查文件大小
  const isLtMaxSize = file.size / 1024 / 1024 < props.maxSize;
  if (!isLtMaxSize) {
    ElMessage.error(`上传图片大小不能超过 ${props.maxSize}MB!`);
    return false;
  }

  // 触发外部自定义验证
  const result = emit("before-upload", file);
  return result !== false;
};

/**
 * 上传成功回调
 */
const handleSuccess = (response: any, file: File, uploadFileList: File[]) => {
  if (response.code === 200) {
    const url = response.data.url || response.data;
    const completeUrl = baseUploadApi(url);

    // 创建文件项
    const fileItem: FileItem = {
      name: file.name,
      path: completeUrl
    };

    if (isMultiple.value) {
      // 多图模式:添加到数组
      let newValue: FileItem[] = [];

      if (Array.isArray(props.modelValue)) {
        newValue = [...props.modelValue];
      }

      newValue.push(fileItem);
      emit("update:modelValue", newValue);
    } else {
      // 单图模式:直接替换
      emit("update:modelValue", completeUrl);
    }

    ElMessage.success("上传成功");
  } else {
    ElMessage.error(response.msg || "上传失败");
  }

  // 触发外部自定义回调
  emit("on-success", response, file, uploadFileList);
};

/**
 * 上传失败回调
 */
const handleError = (error: any, file: File, uploadFileList: File[]) => {
  ElMessage.error("上传失败");
  emit("on-error", error, file, uploadFileList);
};

/**
 * 超出数量限制
 */
const handleExceed = (files: File[], uploadFileList: File[]) => {
  if (isMultiple.value) {
    ElMessage.warning(`最多只能上传${props.limit}张图片`);
    emit("on-exceed", files, uploadFileList);
  }
};

/**
 * 移除图片
 */
const handleRemove = (file: any) => {
  if (isMultiple.value && Array.isArray(props.modelValue)) {
    // 多图模式
    const newValue = props.modelValue.filter(item => {
      if (typeof item === "string") {
        return item !== file.url;
      } else {
        return item.path !== file.url;
      }
    });
    emit("update:modelValue", newValue);
  } else {
    // 单图模式
    emit("update:modelValue", "");
  }
};

/**
 * 计算样式
 */
const uploadStyle = computed(() => {
  return {
    width: props.width ? `${props.width}px` : props.width,
    height: props.height ? `${props.height}px` : props.height
  };
});

/**
 * 手动移除图片
 */
const removeImage = (index: number) => {
  if (isMultiple.value && Array.isArray(props.modelValue)) {
    const newValue = [...props.modelValue];
    newValue.splice(index, 1);
    emit("update:modelValue", newValue);
  } else {
    emit("update:modelValue", "");
  }
};

/**
 * 获取预览图片URL
 */
const getPreviewUrl = (item: any): string => {
  if (typeof item === "string") {
    return item;
  } else if (item && item.path) {
    return item.path;
  } else if (item && item.url) {
    return item.url;
  }
  return "";
};

/**
 * 判断是否有图片
 */
const hasImage = computed(() => {
  if (Array.isArray(props.modelValue)) {
    return props.modelValue.length > 0;
  } else {
    return !!props.modelValue;
  }
});

/**
 * 获取单图模式的图片URL
 */
const singleImageUrl = computed(() => {
  if (!isMultiple.value && typeof props.modelValue === "string") {
    return props.modelValue;
  }
  return "";
});

/**
 * 获取多图模式的图片列表
 */
const multipleImages = computed(() => {
  if (isMultiple.value && Array.isArray(props.modelValue)) {
    return props.modelValue;
  }
  return [];
});

/**
 * 获取上传数量限制
 * 单图模式下设置很大的数字允许连续上传
 */
const uploadLimit = computed(() => {
  if (isMultiple.value) {
    return props.limit;
  } else {
    return 9999; // 单图模式设置很大值允许连续上传替换
  }
});
</script>

<template>
  <div class="re-image-upload">
    <!-- 单图上传模式 -->
    <template v-if="!isMultiple">
      <el-upload
        ref="uploadRef"
        :class="{
          'el-upload--picture-card': !drag,
          're-image-upload__drag': drag
        }"
        :style="uploadStyle"
        :action="action"
        :headers="getHeaders()"
        :multiple="false"
        :limit="uploadLimit"
        :disabled="disabled"
        :accept="accept"
        :show-file-list="false"
        :drag="drag"
        :before-upload="handleBeforeUpload"
        :on-success="handleSuccess"
        :on-error="handleError"
        :on-exceed="handleExceed"
        :on-remove="handleRemove"
      >
        <!-- 已上传图片预览 -->
        <template v-if="singleImageUrl">
          <img
            :src="singleImageUrl"
            class="re-image-upload__preview"
          />
          <div class="re-image-upload__preview-mask">
            <div class="re-image-upload__preview-actions">
              <el-icon
                @click.stop="removeImage(0)"
                class="re-image-upload__delete"
              >
                <delete />
              </el-icon>
            </div>
          </div>
        </template>

        <!-- 未上传时显示上传区域 -->
        <template v-else>
          <!-- 拖拽模式 -->
          <template v-if="drag">
            <el-icon class="el-icon--upload"><upload-filled /></el-icon>
            <div class="el-upload__text">
              {{ tip || "将文件拖到此处,或" }}<em>点击上传</em>
            </div>
          </template>

          <!-- 卡片模式 -->
          <template v-else>
            <el-icon v-if="!icon" class="re-image-upload__icon">
              <plus />
            </el-icon>
            <img v-else :src="icon" class="re-image-upload__custom-icon" />
            <span v-if="tip" class="re-image-upload__tip">{{ tip }}</span>
          </template>
        </template>
      </el-upload>
    </template>

    <!-- 多图上传模式 -->
    <template v-else>
      <!-- 上传按钮 -->
      <el-upload
        ref="uploadRef"
        :class="{
          'el-upload--picture-card': !drag,
          're-image-upload__drag': drag
        }"
        v-model:file-list="multipleImages"
        :style="uploadStyle"
        :action="action"
        :headers="getHeaders()"
        :multiple="true"
        :limit="uploadLimit"
        :disabled="disabled || multipleImages.length >= props.limit"
        :accept="accept"
        :show-file-list="false"
        :drag="drag"
        :before-upload="handleBeforeUpload"
        :on-success="handleSuccess"
        :on-error="handleError"
        :on-exceed="handleExceed"
        :on-remove="handleRemove"
      >
        <!-- 拖拽模式 -->
        <template v-if="drag">
          <el-icon class="el-icon--upload"><upload-filled /></el-icon>
          <div class="el-upload__text">
            <!-- {{ tip || "将文件拖到此处,或" }}<em>点击上传</em> -->
              {{ tip }}
          </div>
        </template>

        <!-- 卡片模式 -->
        <template v-else>
          <el-icon v-if="!icon" class="re-image-upload__icon">
            <plus />
          </el-icon>
          <img v-else :src="icon" class="re-image-upload__custom-icon" />
          <span v-if="tip" class="re-image-upload__tip">{{ tip }}</span>
        </template>
      </el-upload>

      <!-- 多图预览区域 -->
      <div
        v-for="(item, index) in multipleImages"
        :key="index"
        class="re-image-upload__item"
        :style="uploadStyle"
      >
        <img :src="getPreviewUrl(item)" class="re-image-upload__preview" />
        <div class="re-image-upload__preview-mask">
          <div class="re-image-upload__preview-actions">
            <el-icon
              @click.stop="removeImage(index)"
              class="re-image-upload__delete"
            >
              <delete />
            </el-icon>
          </div>
        </div>
      </div>
    </template>
  </div>
</template>

<style lang="scss" scoped>
.el-upload__text{
  font-size: 12px !important;
}
.re-image-upload {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;

  &__icon {
    font-size: 28px;
    color: #8c939d;
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
  }

  &__custom-icon {
    max-width: 60%;
    max-height: 60%;
  }

  &__preview {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  &__preview-mask {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: rgba(0, 0, 0, 0.3);
    opacity: 0;
    transition: opacity 0.3s;

    &:hover {
      opacity: 1;
    }
  }

  &__preview-actions {
    display: flex;
    gap: 8px;
    width: 20px;
    height: 20px;
  }

  &__tip {
    font-size: 12px;
    color: #909399;
    display: block;
    text-align: center;
    margin-top: 6px;
  }

  &__drag {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    border: 1px dashed #d9d9d9;
    border-radius: 6px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
    transition: border-color 0.3s;

    &:hover {
      border-color: var(--el-color-primary);
    }
  }

  &__item {
    position: relative;
    border-radius: 6px;
    overflow: hidden;
    border: 1px solid #d9d9d9;

    &:hover .re-image-upload__preview-mask {
      opacity: 1;
    }
  }

  &__delete {
    font-size: 16px;
    color: #fff;
    cursor: pointer;
    padding: 2px;
    background-color: rgba(0, 0, 0, 0.5);
    border-radius: 50%;
    width: 20px;
    height: 20px;
    display: flex;
    align-items: center;
    justify-content: center;

    &:hover {
      color: #f56c6c;
      transform: scale(1.1);
    }
  }
}

:deep(.el-upload--picture-card) {
  position: relative;

  .re-image-upload__preview {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
}

:deep(.el-upload-dragger) {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  border: none;
}
</style>

demo

<script lang="ts" setup>
/**
 * ReImageUpload 组件使用示例
 */
import { ref } from "vue";
import ReImageUpload from "./index.vue";
import { ElMessage } from "element-plus";
import { baseUrlApi } from "@/api/utils";

// 单图上传示例
const singleImageUrl = ref("");

// 多图上传示例
const multipleImageUrls = ref<string[]>([]);

// 自定义上传前验证
const beforeUploadHandler = (file: File) => {
  const isJPG = file.type === "image/jpeg";
  if (!isJPG) {
    ElMessage.error("上传图片只能是JPG格式!");
    return false;
  }
  return true;
};

// 自定义上传成功回调
const onSuccessHandler = (response, file, fileList) => {
  console.log("自定义上传成功回调", response, file, fileList);
  ElMessage.success("图片上传成功!");
};

// 自定义上传接口
const customAction = baseUrlApi("/file/imageCustom");

// 自定义请求头
const customHeaders = {
  "X-Custom-Header": "自定义请求头"
};

// 自定义超出限制回调
const onExceedHandler = (files, fileList) => {
  ElMessage.warning("超出数量限制,最多上传3张图片!");
};
</script>

<template>
  <div class="demo-container">
    <h2>ReImageUpload 图片上传组件示例</h2>
    
    <div class="demo-section">
      <h3>基础用法</h3>
      <ReImageUpload v-model="singleImageUrl" />
      <div class="demo-value">图片URL: {{ singleImageUrl }}</div>
    </div>
    
    <div class="demo-section">
      <h3>拖拽上传</h3>
      <ReImageUpload 
        v-model="singleImageUrl" 
        drag 
        width="300px"
        height="200px"
        tip="拖拽图片到此处上传"
      />
    </div>
    
    <div class="demo-section">
      <h3>多图上传</h3>
      <ReImageUpload 
        v-model="multipleImageUrls" 
        :limit="3"
        @on-exceed="onExceedHandler"
      />
      <div class="demo-value">图片URLs: {{ multipleImageUrls }}</div>
    </div>
    
    <div class="demo-section">
      <h3>自定义验证和回调</h3>
      <ReImageUpload 
        v-model="singleImageUrl" 
        @before-upload="beforeUploadHandler"
        @on-success="onSuccessHandler"
      />
    </div>
    
    <div class="demo-section">
      <h3>自定义尺寸和图标</h3>
      <ReImageUpload 
        v-model="singleImageUrl" 
        width="120px"
        height="120px"
        icon="/path/to/custom-icon.png"
        tip="点击上传自定义图标"
      />
    </div>
    
    <div class="demo-section">
      <h3>自定义接口和请求头</h3>
      <ReImageUpload 
        v-model="singleImageUrl" 
        :action="customAction"
        :headers="customHeaders"
        tip="使用自定义上传接口"
      />
    </div>
  </div>
</template>

<style lang="scss" scoped>
.demo-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  
  h2 {
    margin-bottom: 20px;
    font-size: 24px;
    color: #303133;
  }
  
  .demo-section {
    margin-bottom: 30px;
    padding: 20px;
    border: 1px solid #ebeef5;
    border-radius: 4px;
    
    h3 {
      margin-bottom: 15px;
      font-size: 18px;
      color: #606266;
    }
    
    .demo-value {
      margin-top: 10px;
      padding: 10px;
      background-color: #f5f7fa;
      border-radius: 4px;
      font-family: monospace;
      word-break: break-all;
    }
  }
}
</style> 

预览

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值