基于Vue3+Ts+Vant的高级图片上传组件

大家好,我是鱼樱!!!

关注公众号【鱼樱AI实验室】持续每天分享更多前端和AI辅助前端编码新知识~~喜欢的就一起学反正开源至上,无所谓被诋毁被喷被质疑文章没有价值~

一个城市淘汰的自由职业-农村前端程序员(虽然不靠代码挣钱,写文章就是为爱发电),兼职远程上班目前!!!热心坚持分享~~~

以下是一个基于Vant封装的通用图片上传组件,支持单张和多张图片上传、预览和旋转,并具有良好的扩展性。

组件代码 (ImageUploader.vue)

html 复制代码
<template>
  <div class="v-image-uploader">
    <!-- 主上传组件 -->
    <van-uploader
      v-model="innerFileList"
      v-bind="$attrs"
      :max-count="maxCount"
      :max-size="maxSize"
      :multiple="multiple"
      :disabled="disabled"
      :readonly="readonly"
      :deletable="deletable"
      :before-read="handleBeforeRead"
      @oversize="$emit('oversize', $event)"
      @delete="handleDelete"
      @click-preview="handlePreview"
    >
      <!-- 传递上传区域默认插槽 -->
      <template #default>
        <slot>
          <!-- 默认上传区域 -->
          <div class="upload-area" v-if="showUploadArea">
            <van-icon name="photograph" size="24" />
            <div class="upload-text">{{ uploadText }}</div>
          </div>
        </slot>
      </template>

      <!-- 传递预览覆盖插槽 -->
      <template #preview-cover="{ file, index }">
        <slot name="preview-cover" :file="file" :index="index"></slot>
      </template>

      <!-- 传递其他所有插槽 -->
      <template v-for="(_, name) in $slots" :key="name" #[name]="slotData" v-if="name !== 'default' && name !== 'preview-cover'">
        <slot :name="name" v-bind="slotData"></slot>
      </template>
    </van-uploader>

    <!-- 错误信息 -->
    <div v-if="errorMsg" class="upload-error">{{ errorMsg }}</div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch, useAttrs } from 'vue';
import { showImagePreview, showToast } from 'vant';
import type { UploaderFileListItem, ImagePreviewOptions } from 'vant';

// 定义组件属性
interface Props {
  /**
   * 已上传的文件列表
   * 支持字符串数组 ['url1', 'url2'] 或对象数组 [{url: 'url1'}, {url: 'url2'}]
   */
  modelValue: (string | UploaderFileListItem)[];
  
  /**
   * 是否多选图片
   * @default false
   */
  multiple?: boolean;
  
  /**
   * 最大上传数量
   * @default Infinity
   */
  maxCount?: number;
  
  /**
   * 文件大小限制,单位为字节
   * @default Infinity
   */
  maxSize?: number;
  
  /**
   * 允许的文件格式扩展名数组,不带点,如 ['jpg', 'jpeg', 'png']
   * @default ['jpg', 'jpeg', 'png', 'gif', 'webp']
   */
  formats?: string[];
  
  /**
   * 是否显示删除按钮
   * @default true
   */
  deletable?: boolean;
  
  /**
   * 是否禁用
   * @default false
   */
  disabled?: boolean;
  
  /**
   * 是否只读状态
   * @default false
   */
  readonly?: boolean;
  
  /**
   * 是否开启图片预览
   * @default true
   */
  preview?: boolean;
  
  /**
   * 上传区域的文字
   * @default "上传图片"
   */
  uploadText?: string;
  
  /**
   * 图片预览配置
   */
  previewOptions?: Partial<ImagePreviewOptions>;
  
  /**
   * 格式错误提示文本
   * @default "文件格式不正确,请上传{formats}格式的图片"
   */
  formatErrorMsg?: string;
}

// 定义默认属性值
const props = withDefaults(defineProps<Props>(), {
  multiple: false,
  maxCount: Infinity,
  maxSize: Infinity,
  formats: () => ['jpg', 'jpeg', 'png', 'gif', 'webp'],
  deletable: true,
  disabled: false,
  readonly: false,
  preview: true,
  uploadText: '上传图片',
  previewOptions: () => ({}),
  formatErrorMsg: ''
});

// 定义事件
const emit = defineEmits([
  'update:modelValue',
  'oversize',
  'delete',
  'error',
  'success',
  'preview'
]);

// 获取原生属性
const attrs = useAttrs();

// 内部状态
const innerFileList = ref<UploaderFileListItem[]>([]);
const errorMsg = ref('');

// 计算是否显示上传区域(当达到最大数量时隐藏)
const showUploadArea = computed(() => {
  return innerFileList.value.length < props.maxCount;
});

// 格式化错误信息
const formattedFormatErrorMsg = computed(() => {
  if (props.formatErrorMsg) return props.formatErrorMsg;
  return `文件格式不正确,请上传${props.formats.join('/')}格式的图片`;
});

// 将字符串数组转换为文件列表对象数组
const convertToFileList = (value: (string | UploaderFileListItem)[]) => {
  return value.map(item => {
    if (typeof item === 'string') {
      return { url: item };
    }
    return item;
  });
};

// 监听modelValue变化
watch(
  () => props.modelValue,
  (newVal) => {
    innerFileList.value = convertToFileList(newVal);
  },
  { immediate: true, deep: true }
);

// 监听内部文件列表变化,同步到父组件
watch(
  innerFileList,
  (newVal) => {
    emit('update:modelValue', newVal);
  },
  { deep: true }
);

/**
 * 在读取文件前验证格式
 * @param file 文件对象
 * @returns 是否通过验证
 */
const handleBeforeRead = (file: File | File[]) => {
  errorMsg.value = '';
  const files = Array.isArray(file) ? file : [file];
  
  // 验证文件格式
  for (const file of files) {
    // 获取文件扩展名
    const extension = file.name.split('.').pop()?.toLowerCase() || '';
    
    if (!props.formats.includes(extension)) {
      errorMsg.value = formattedFormatErrorMsg.value;
      showToast(errorMsg.value);
      emit('error', { file, message: errorMsg.value });
      return false;
    }
  }
  
  // 验证通过
  emit('success', files.length === 1 ? files[0] : files);
  return true;
};

/**
 * 处理文件删除
 * @param file 被删除的文件
 * @param detail 删除的详细信息
 */
const handleDelete = (file: UploaderFileListItem, detail: { index: number }) => {
  emit('delete', file, detail);
};

/**
 * 处理图片预览
 * @param file 被预览的文件
 * @param detail 预览的详细信息
 */
const handlePreview = (file: UploaderFileListItem, detail: { index: number }) => {
  if (!props.preview) return;
  
  // 获取所有预览图片URL
  const images = innerFileList.value.map(item => item.url || '');
  
  // 合并默认预览选项和用户自定义选项
  const options: ImagePreviewOptions = {
    images,
    startPosition: detail.index,
    showIndex: true,
    closeable: true,
    showIndicators: images.length > 1,
    // 允许图片旋转
    swipeDuration: 300,
    ...props.previewOptions
  };
  
  // 调用Vant图片预览组件
  showImagePreview(options);
  
  // 触发预览事件
  emit('preview', file, detail);
};

// 对外暴露方法
defineExpose({
  // 清空上传列表
  clear: () => {
    innerFileList.value = [];
  },
  // 手动触发图片预览
  preview: (index = 0) => {
    if (innerFileList.value[index]) {
      handlePreview(innerFileList.value[index], { index });
    }
  }
});
</script>

<style scoped>
.v-image-uploader {
  width: 100%;
}

.upload-area {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  padding: 16px;
  box-sizing: border-box;
}

.upload-text {
  margin-top: 8px;
  font-size: 12px;
  color: #969799;
}

.upload-error {
  margin-top: 8px;
  font-size: 12px;
  color: #ee0a24;
}
</style>

使用示例

html 复制代码
<template>
  <div class="upload-demo">
    <h2>单张图片上传</h2>
    <v-image-uploader
      v-model="singleImage"
      :max-count="1"
      :max-size="2 * 1024 * 1024"
      :formats="['jpg', 'jpeg', 'png']"
      upload-text="上传头像"
      @oversize="onOversize"
      @error="onError"
      @success="onSuccess"
    />
    
    <h2>多张图片上传</h2>
    <v-image-uploader
      v-model="multipleImages"
      multiple
      :max-count="9"
      :max-size="5 * 1024 * 1024"
      upload-text="上传图片集"
      :preview-options="previewOptions"
    >
      <!-- 自定义上传区域 -->
      <div class="custom-upload">
        <van-icon name="plus" size="22" />
        <span>添加图片</span>
      </div>
      
      <!-- 自定义预览覆盖层 -->
      <template #preview-cover="{ index }">
        <div class="preview-cover">
          <span class="index">{{ index + 1 }}</span>
        </div>
      </template>
    </v-image-uploader>
    
    <!-- 操作按钮 -->
    <div class="actions">
      <van-button type="primary" @click="clearAll">清空所有</van-button>
      <van-button type="primary" @click="previewFirst">预览第一张</van-button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { showToast } from 'vant';
import VImageUploader from '@/components/VImageUploader.vue';
import type { UploaderFileListItem } from 'vant';

// 单张图片数据
const singleImage = ref<UploaderFileListItem[]>([]);

// 多张图片数据
const multipleImages = ref<UploaderFileListItem[]>([
  { url: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg' },
  { url: 'https://fastly.jsdelivr.net/npm/@vant/assets/leaf.jpeg' }
]);

// 图片预览配置
const previewOptions = {
  showIndex: true,
  closeable: true,
  closeIconPosition: 'top-right' as const,
  beforeClose: () => {
    return true; // 返回true允许关闭,返回false阻止关闭
  }
};

// 组件引用,用于调用组件方法
const uploaderRef = ref();

// 文件超出大小限制回调
const onOversize = (file: File | File[]) => {
  const files = Array.isArray(file) ? file : [file];
  showToast(`文件 ${files.map(f => f.name).join(', ')} 大小超出限制`);
};

// 上传错误回调
const onError = ({ file, message }: { file: File, message: string }) => {
  console.error('上传错误:', message, file);
};

// 上传成功回调
const onSuccess = (file: File | File[]) => {
  showToast('上传成功');
  console.log('上传成功:', file);
};

// 清空所有图片
const clearAll = () => {
  singleImage.value = [];
  multipleImages.value = [];
};

// 预览第一张图片
const previewFirst = () => {
  if (multipleImages.value.length > 0) {
    // 通过组件实例调用preview方法
    uploaderRef.value?.preview(0);
  } else {
    showToast('没有可预览的图片');
  }
};
</script>

<style scoped>
.upload-demo {
  padding: 16px;
}

h2 {
  margin: 16px 0;
  font-size: 16px;
}

.custom-upload {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  color: #969799;
}

.custom-upload span {
  margin-top: 8px;
  font-size: 12px;
}

.preview-cover {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: rgba(0, 0, 0, 0.3);
}

.preview-cover .index {
  color: #fff;
  font-size: 12px;
  background-color: rgba(0, 0, 0, 0.5);
  padding: 2px 6px;
  border-radius: 4px;
}

.actions {
  margin-top: 20px;
  display: flex;
  gap: 10px;
}
</style>

注册全局组件 (main.ts)

typescript 复制代码
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { Button, Icon, Uploader, Toast } from 'vant';
import VImageUploader from '@/components/VImageUploader.vue';

const app = createApp(App);

// 注册Vant组件
app.use(Button);
app.use(Icon);
app.use(Uploader);
app.use(Toast);

// 注册自定义上传组件
app.component('VImageUploader', VImageUploader);

app.mount('#app');

组件特点

  1. 强大的格式限制:

    • 支持通过formats属性指定允许的图片格式
    • 提供友好的格式错误提示
  2. 灵活的单/多图片上传:

    • 通过multiple属性控制是否允许多选
    • 使用maxCount属性限制最大上传数量
  3. 高性能图片预览和旋转:

    • 利用Vant的ImagePreview组件实现预览和旋转功能
    • 通过previewOptions属性自定义预览行为
  4. 完整的事件支持:

    • 支持上传成功、失败、超出大小限制等关键事件
    • 继承Vant原有事件系统
  5. 灵活的插槽系统:

    • 支持自定义上传区域
    • 支持自定义预览覆盖层
    • 传递所有Vant原生插槽
  6. 扩展性强:

    • 继承了Vant Uploader的所有属性
    • 提供了额外的扩展属性和方法
    • 支持通过类型系统进行严格类型检查
  7. 简单易用:

    • 使用v-model进行双向数据绑定
    • 提供清晰的组件API和类型定义
    • 详细的代码注释

这个组件充分利用了Vue3的新特性和Vant组件库的能力,同时提供了更强大的功能和更好的开发体验。无论是简单的单图上传还是复杂的多图管理,都能轻松实现。

相关推荐
Slow菜鸟39 分钟前
ES5 vs ES6:JavaScript 演进之路
前端·javascript·es6
小冯的编程学习之路41 分钟前
【前端基础】:HTML
前端·css·前端框架·html·postman
Jiaberrr2 小时前
Vue 3 中搭建菜单权限配置界面的详细指南
前端·javascript·vue.js·elementui
科科是我嗷~2 小时前
【uniapp】textarea maxlength字数计算不准确的问题
javascript·uni-app·html
懒大王95272 小时前
uniapp+Vue3 组件之间的传值方法
前端·javascript·uni-app
烛阴3 小时前
秒懂 JSON:JavaScript JSON 方法详解,让你轻松驾驭数据交互!
前端·javascript
拉不动的猪3 小时前
刷刷题31(vue实际项目问题)
前端·javascript·面试
zeijiershuai3 小时前
Ajax-入门、axios请求方式、async、await、Vue生命周期
前端·javascript·ajax
恋猫de小郭3 小时前
Flutter 小技巧之通过 MediaQuery 优化 App 性能
android·前端·flutter
只会写Bug的程序员3 小时前
面试之《webpack从输入到输出经历了什么》
前端·面试·webpack