# 二次封装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>

Last Updated: 5/15/2023, 9:31:21 PM