基于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>
组件参数
参数名 | 类型 | 默认值 | 说明 |
---|
modelValue | string | string[] | '' | 绑定值,单图时为字符串,多图时为字符串数组 |
drag | boolean | false | 是否启用拖拽上传 |
limit | number | 1 | 上传数量限制 |
width | string | number | '178px' | 上传区域宽度 |
height | string | number | '178px' | 上传区域高度 |
icon | string | '' | 自定义图标URL |
action | string | /file/image | 上传接口URL |
headers | Record<string, string> | {} | 自定义请求头 |
maxSize | number | 2 | 上传文件大小限制(MB) |
showFileList | boolean | false | 是否显示文件列表 |
accept | string | 'image/jpg,image/jpeg,image/png,image/gif' | 文件类型限制 |
tip | string | '点击上传图片' | 提示文字 |
disabled | boolean | false | 是否禁用 |
事件
事件名 | 参数 | 说明 |
---|
update:modelValue | string | 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>
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<{
modelValue?: string | FileItem[];
drag?: boolean;
limit?: number;
width?: string | number;
height?: string | number;
icon?: string;
action?: string;
headers?: Record<string, string>;
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[]>([]);
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 = () => {
const defaultHeaders = {
Authorization: formatToken(getToken().token)
};
return {
...defaultHeaders,
...props.headers
};
};
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", "");
}
};
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;
}
});
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>
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>
预览
