# 二次封装ant多张图片上传组件
# 组件props
props | 描述 |
---|---|
fileList | 双向绑定的 fileList 值,使用示例: `v-model:file-list="fileList" |
api | 上传图片的 api 方法,一般项目上传都是同一个 api 方法,在组件里直接引入即可(非必传) |
drag | 是否支持拖拽上传图片,默认为 true |
disabled | 是否禁用 上传、删除 功能,可查看图片 |
fileSize | 单个图片文件大小限制,默认为 5M |
fileType | 图片类型限制,默认类型为 ["image/jpeg", "image/png", "image/jpg"] |
height | 组件高度样式,默认为 "150px" |
width | 组件宽度样式,默认为 "150px" |
borderRadius | 组件边框圆角样式,默认为 "8px" |
bgColor | 组件背景颜色 |
# 代码
<template>
<div class="upload-box">
<a-upload
action="#"
:drag="drag"
:multiple="true"
:maxCount="maxCount"
list-type="picture-card"
v-model:file-list="fileList"
:disabled="self_disabled"
:show-upload-list="true"
:accept="fileType.join(',')"
:customRequest="customRequest"
:before-upload="beforeUpload"
:on-success="uploadSuccess"
:on-change="handleOnchange"
>
<div class="upload-empty">
<PlusOutlined />
<span>上传图片</span>
</div>
<template #itemRender="{ file }">
<div class="thumbnail">
<template v-if="file.status === 'uploading'">
<div class="up-progress">
<a-progress type="circle" :width="80" :percent="file.percent" status="active" />
</div>
</template>
<template v-else>
<img :src="file.url" class="upload-image" />
<div class="upload-handle" @click.stop>
<div class="handle-icon" @click="handlePictureCardPreview(file)">
<EyeOutlined />
<span>查看</span>
</div>
<div class="handle-icon" @click="handleRemove(file)" v-if="!self_disabled">
<DeleteOutlined />
<span>删除</span>
</div>
</div>
</template>
</div>
<div class="remark">
<span>备注</span>
<a-textarea show-count v-model:value="file.remark" placeholder="备注" :auto-size="{ minRows: 1, maxRows: 2 }" :maxlength="100" />
</div>
</template>
</a-upload>
<div class="upload-tip">
<slot name="tip"></slot>
</div>
<a-modal :visible="previewVisible" :footer="null" @cancel="handleCancel">
<img alt="example" style="width: 100%" :src="viewImageUrl" />
</a-modal>
</div>
</template>
<script lang="ts">
export default {
name: "UploadImgs",
};
</script>
<script setup lang="ts">
import { ref, computed } from "vue";
import { message, Upload } from "ant-design-vue";
import type { UploadProps } from "ant-design-vue";
import { uploadImg } from "@/api/modules/qbsc/index";
import { CloseOutlined, PlusOutlined, EyeOutlined, CloseCircleOutlined, DeleteOutlined, EditOutlined } from "@ant-design/icons-vue";
import { FileTypes, IUploadFile } from "./types";
interface UploadFileProps {
api?: (params: any, callback: Function) => Promise<any>; // 上传图片的 api 方法,一般项目上传都是同一个 api 方法,在组件里直接引入即可 ==> 非必传
drag?: boolean; // 是否支持拖拽上传 ==> 非必传(默认为 true)
disabled?: boolean; // 是否禁用上传组件 ==> 非必传(默认为 false)
fileSize?: number; // 图片大小限制 ==> 非必传(默认为 5M)
fileType?: FileTypes[]; // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/jpg"])
height?: string; // 组件高度 ==> 非必传(默认为 150px)
width?: string; // 组件宽度 ==> 非必传(默认为 150px)
borderRadius?: string; // 组件边框圆角 ==> 非必传(默认为 8px)
bgColor?: string;
maxCount?: number;
fileList: IUploadFile[];
}
// 接受父组件参数
const props = withDefaults(defineProps<UploadFileProps>(), {
drag: true,
disabled: false,
fileSize: 5,
fileType: () => ["image/jpeg", "image/png", "image/gif"],
height: "150px",
width: "150px",
borderRadius: "8px",
bgColor: "#fff",
maxCount: 100,
fileList: () => [],
});
const fileList = ref<IUploadFile[]>(props.fileList);
// 判断是否禁用上传和删除
const self_disabled = computed(() => {
return props.disabled;
});
/**
* @description 文件上传之前判断
* @param rawFile 选择的文件
* */
const beforeUpload: UploadProps["beforeUpload"] = (rawFile: File) => {
const limitFlag = rawFile.size / 1024 / 1024 < props.fileSize;
if (!limitFlag) {
message.warning("单张图片上传大小限制为5M");
return false;
}
const imgType = props.fileType.includes(rawFile.type as FileTypes);
if (!imgType) {
message.warning("上传图片不符合所需的格式!");
return imgType || Upload.LIST_IGNORE;
}
// 判断文件是否已被上传
const hasUploaded = fileList.value?.some(item => {
return item.name === rawFile.name;
});
if (hasUploaded) {
message.warning(`${rawFile.name}文件已上传`);
return !hasUploaded || Upload.LIST_IGNORE;
}
};
/**
* @description 图片上传
* @param options upload 所有配置项
* */
interface UploadEmits {
(e: "update:fileList", value: IUploadFile[]): void;
}
const emit = defineEmits<UploadEmits>();
const customRequest = async ({ file, onProgress, onSuccess, onError }) => {
let formData = new FormData();
formData.append("file", file);
try {
const api = props.api ?? uploadImg;
const { data } = await api(formData, (percent: any) => {
onProgress({ percent });
});
onSuccess(data);
} catch (error) {
onError(error as any);
}
};
const uploadSuccess = (response: { url: string; name: string } | undefined, uploadFile: IUploadFile) => {
if (!response) return;
uploadFile.url = response.url;
// uploadFile.status = "done"
uploadFile.remark = "";
fileList.value = fileList.value.map(item => {
if (item.name === response.name) {
item = uploadFile;
}
return item;
});
emit("update:fileList", fileList.value);
message.success("图片上传成功");
};
const handleOnchange = ({ file, fileList }) => {
// if (file.status !== "uploading") {
// console.log(file, fileList);
// }
// if (file.status === "done") {
// console.log(`${file.name} 文件上传成功`);
// } else if (file.status === "error") {
// message.error(`${file.name} 文件上传失败`);
// }
};
/**
* @description 预览图片
*/
const previewVisible = ref(false);
const viewImageUrl = ref("");
const handlePictureCardPreview: UploadProps["onPreview"] = file => {
viewImageUrl.value = file.url!;
previewVisible.value = true;
};
/**
* @description 取消预览
* */
const handleCancel = () => {
previewVisible.value = false;
};
/**
* @description 删除图片
* */
const handleRemove = (file: IUploadFile) => {
fileList.value = fileList.value.filter(item => item.url !== file.url || item.name !== file.name);
emit("update:fileList", fileList.value);
};
</script>
<style lang="less" scoped>
.upload-box {
position: relative;
}
:deep(.ant-upload-list-picture-card) {
display: flex;
justify-content: flex-start;
align-items: center;
flex-wrap: wrap;
}
:deep(.ant-upload-list-picture-card-container) {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: v-bind(width) !important;
height: v-bind(height) !important;
overflow: hidden;
border: 1px dashed #eee;
border-radius: v-bind(borderRadius);
transition: all 300ms;
user-select: none;
cursor: pointer;
background: v-bind(bgColor);
.thumbnail {
width: 100%;
height: calc(v-bind(height) - 100px);
position: relative;
margin-bottom: 10px;
&:hover {
border-color: #eee;
.upload-handle {
opacity: 1;
}
}
}
}
:deep(.ant-upload) {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: v-bind(width);
height: v-bind(height);
overflow: hidden;
border: 1px dashed #eee;
border-radius: v-bind(borderRadius);
transition: all 300ms;
user-select: none;
cursor: pointer;
background: v-bind(bgColor);
&:hover {
border-color: #eee;
.upload-handle {
opacity: 1;
}
}
}
.upload-image {
width: 100%;
height: 100%;
object-fit: contain;
}
.upload-empty {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 12px;
line-height: 30px;
}
.upload-handle {
position: absolute;
top: 0;
right: 0;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
cursor: pointer;
background: rgb(0 0 0 / 60%);
opacity: 0;
transition: all 300ms;
.handle-icon {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 6%;
color: aliceblue;
text-align: center;
letter-spacing: 1px;
.anticon {
margin-bottom: 40%;
font-size: 130%;
line-height: 130%;
}
span {
font-size: 85%;
line-height: 85%;
}
}
}
.upload-tip {
text-align: center;
color: #595959;
font-size: 16px;
line-height: 2em;
width: 200px;
color: #bfbfbf;
// display: inline-block;
}
.remark {
width: 90%;
margin: auto;
height: 100px;
}
:deep(.ant-upload-select-picture-card) {
width: 200px;
height: 200px;
position: relative;
display: flex;
flex-direction: column;
&:hover{
border-color: #2f54eb;
}
&::after {
display: block;
content: "支持jpg、jpeg、png格式,单张图片≤5M";
font-size: 14px;
color: #bfbfbf;
margin-top: 16px;
cursor: default;
}
}
.up-progress {
width: 90%;
text-align: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>