[封装自己的ui组件库] upload的实现与难点

效果

1、服务文件(tmp为保存上传文件文件夹)

fw.png

2、点击上传

QQ录屏20221118001557.gif

3、图片列表

QQ录屏20221118001912.gif

4、拖拽

QQ录屏20221118002147.gif

5、手动上传

QQ录屏20221118002010.gif

5、上传失败

QQ截图20221118002300.png

6、服务

QQ截图20221118002234.png

问题

1、如何打开文件列表

2、如何取出文件

3、对取出的文件校验?

4、如何发送请求(多文件上传?)

5、如何完成上传列表展示

6、拖拽上传?

7、可扩展?

思路

在实现的过程中大致思考了一下流程

1 点击或取文件(input:file)

2 获取文件进行校验(大小和类型)

3 创建初始化列表(对上传文件进行包装)

4 根据是否自动上传决定是否直接添加上传列表,还是手动上传至上传列表

5 监视初始化列表,取出上传列表数据,使用formdata转换为二进制数据通过ajax发送至后台

6 请求过程中监视请求结果动态展示列表

7 多文件使用原生的multipart可实现

8 拖拽可使用drop,dragover,dragenter,dragleave,dragover的事件

。。。。

实现

1、利用input:file获取文件

<input ref="inputFile" type="file" name="" id="" @change="selectfilesChange" :multiple="multiple">

 // 点击触发弹窗的事件
        pushUploadFrame() {
            // 清空inputFile
            this.$refs.inputFile.value = null

            this.$refs.inputFile.click();
        },

2、取出文件+文件校验

 // 文件选中变化
        selectfilesChange(e, files) {

            if (e) {
                // 转换为真实数组以便使用数组的方法
                this.selectFileList = Array.from(e.target.files);
            } else {
                this.selectFileList = Array.from(files);
                console.log(files);
            }

            // 判断文件是否已上传

            // 前去校验
            let { isResult, content } = this.handlerFileAccept(this.selectFileList);

            if (isResult) {
                // 文件校验成功事件
                this.$emit('acceptAfter', this.selectFileList)

                // 添加到初始化上传列表
                this.addUpList(this.selectFileList)
            } else {
                // 触发文件上传失败事件
                this.$emit('uploadError', content)
            }
        },
        
        // 处理文件校验相关
        handlerFileAccept(accEptList) {
            // console.log(accEptList);
            // 没有文件时不做处理
            if (!accEptList.length) return;

            let { size, accept } = this;
            // 类型校验分割为数组
            let accepts = accept.split(',')
            // 校验处理
            let sizeResult = accEptList.every(child => child.size <= size);
            // 类型校验
            let typeResult = accEptList.every(child => {
                if (/\/\*$/.test(accepts)) {
                    return true
                } else {
                    return accepts.includes(child.type)
                }
            })

            if (sizeResult && typeResult) {
                return { isResult: true, content: '文件校验成功' }
            } else if (!typeResult) {
                return { isResult: false, content: `文件类型不符合要求, 需要${accept}类型文件` }
            } else if (!sizeResult) {
                return { isResult: false, content: `文件大小不得超过${size / 1024}kb` }
            }

        },

3、对初始化列表的数据进行包装

 // 添加到上传列表
        addUpList(files) {
            // 生成符合上传要求的文件
            let uploadList = files.map((child, index) => {
                // console.log(child);
                // 生成传输请求格式
                return {
                    header: this.headers, //  请求头部
                    withCredentials: this.withCredentials, // 是否允许cooike
                    uid: Date.now() + index,
                    // status: 0, // 上传状态 // 0:就绪, 1: 上传中, -1 :上传失败,2:上传成功
                    file: child, // 文件
                    size: child.size, // 文件大小
                    name: child.name, // 文件名称
                    type: child.type, // 文件类型
                    onProgress: (e, uid) => {
                        // 监视上传进度
                        // console.log('上传中', e);
                        this.$emit('uploadProgress', e, child)
                        this.$children.filter(item => item.$el.className == 'fileList')[0].uploadPropress(e, uid)
                    },
                    onError: (err, uid) => {
                        // 上传失败触发的事件
                        // console.log('上传失败', err, child);
                        this.$emit('uploadError', err)
                        this.$children.filter(item => item.$el.className == 'fileList')[0].uploadError(uid)
                    },
                    onSuccess: (res, uid) => {
                        // 上传成功后触发的事件
                        // console.log('上传成功', res, child);
                        this.$emit('uploadSuccess', res, child)
                        this.$children.filter(item => item.$el.className == 'fileList')[0].uploadSuccess(uid)
                    }
                }
            })


            if (this.autoUpload) {
                // 添加到生成的上传文件列表中
                uploadList.forEach(item => {
                    this.initFileList.push(item)
                });
            } else {
                uploadList.forEach(item => {
                    this.uploadFileList.unshift(item)
                })
            }

            // 清空添加的文件列表
            this.clearFileList()
        },

4 监视初始化列表,一旦有数据将其添加入上传列表

initFileList: {
            handler(newV) {
                // console.log('待上传', newV);
                if (newV.length && this.autoUpload) {
                    // console.log('触发上传');
                    // 取出第一个文件
                    let startFile = this.initFileList.shift()

                    // 触发上传方法
                    this.upload(startFile)
                } else {
                    console.log('初始化列表没有待上传的文件了');
                }
            }, immediate: true
        }

5、创建请求并发送

  // 上传方法
        upload(file) {
            if (this.autoUpload) {
                // 添加到上传列表
                this.uploadFileList.unshift(file)
            }

            //创建 xhr
            const xhr = new XMLHttpRequest();

            // 配置请求方法和路径
            xhr.open('POST', this.action)

            // 上传成功
            xhr.addEventListener('load', () => {
                if (xhr.status < 200 || xhr.status >= 300) {
                    return file.onError('未知错误', file.uid);
                }
                // 触发上传成功
                file.onSuccess(xhr.response, file.uid);
            })

            // 上传失败
            xhr.addEventListener('error', (e) => {
                file.onError(e, file.uid)
            })

            // 监视上传进度
            if (xhr.upload) {
                xhr.upload.onprogress = function progress(e) {
                    if (e.total > 0) {
                        e.percent = e.loaded / e.total * 100;
                    }
                    file.onProgress(e, file.uid);
                };
            }

            // 转换为二进制文件
            let fd = new FormData()
            fd.append(file.name, file.file)

            // 携带的额外参数
            if (this.data) {
                for (const key in this.data) {
                    if (Object.hasOwnProperty.call(this.data, key)) {
                        fd.append(key, this.data[key])
                    }
                }
            }

            // 上传的文件
            xhr.send(fd)
        },

6、动态展示列表

<template>
    <div class='fileList' :style="{ 'max-width': maxWidth + 'px' }">
        <!-- 文件列表 -->
        <template v-if="!preView">
            <div class="fileDetail" v-for="item in uploadList" :key="item.uid" :data-uid="item.uid">
                <!-- 左侧文件图标 -->
                <span class="leftIcon">
                    <tyIcon type="file"></tyIcon>
                </span>

                <!-- 文件名称 -->
                <p class="detail" :title="item.name" v-text="item.name"></p>

                <!-- 右侧成功/删除图标 -->
                <span class="rightIcon">
                    <tyIcon type="close" @click.native="handlerDeteleFile(item.uid)"></tyIcon>
                    <tyIcon type="success" v-if="item.isSuccess" style="color:#94d82d;"></tyIcon>
                    <tyIcon type="warring" v-if="item.isError" style="color:#ffa94d;"></tyIcon>
                </span>
            </div>
        </template>

        <template v-else>
            <div class="preView" v-for="item in uploadList" :key="item.uid" :data-uid="item.uid">
                <!-- 左侧文件图标 -->
                <div class="view">
                    <img class="img" :src='item.base64' />
                </div>

                <!-- 文件名称 -->
                <p class="detail" :title="item.name" v-text="item.name"></p>

                <!-- 右侧成功/删除图标 -->
                <span class="rightIcon">
                    <tyIcon type="close" @click.native="handlerDeteleFile(item.uid)"></tyIcon>
                    <tyIcon type="success" v-if="item.isSuccess" style="color:#94d82d;"></tyIcon>
                    <tyIcon type="warring" v-if="item.isError" style="color:#ffa94d;"></tyIcon>
                </span>
            </div>
        </template>
    </div>
</template>

<script>
import tyIcon from '../../icon/src/main.vue'
export default {
    name: 'fileList',
    props: {
        files: {
            type: Array,
            default: () => []
        },
        // 最大宽度
        maxWidth: {
            type: [Number]
        },
        // 删除文件事件
        deleteFile: {
            type: Function
        },
        preView: {
            type: Boolean,
            default: false
        }
    },

    data() {
        return {
            uploadList: []
        }
    },

    methods: {
        // 点击删除图标触发
        handlerDeteleFile(uid) {
            // 触发对应的删除方法
            this.$emit('deleteFile', uid)
        },
        // 触发更改进度
        uploadPropress(e, uid) {
            console.log(e, uid);
        },
        // 上传成功
        uploadSuccess(uid) {
            this.uploadList = this.uploadList.map(child => {
                if (child.uid == uid) {
                    child.isSuccess = true
                }
                return child
            })
        },
        // 上传失败
        uploadError(uid) {
            this.uploadList = this.uploadList.map(child => {
                if (child.uid == uid) {
                    child.isError = true
                }
                return child
            })

            // 删除上传的文件
        },
    },

    watch: {
        files: {
            handler(newV) {
                if (this.preView) {
                    this.uploadList = newV.map(child => {
                        if (child.file) {
                            let reader = new FileReader();
                            reader.readAsDataURL(child.file); //将文件读取为 DataURL,也就是base64编码
                            reader.onload = function (ev) { //文件读取成功完成时触发
                                var dataURL = ev.target.result; //获得文件读取成功后的DataURL,也就是base64编码
                                child.base64 = dataURL
                            }
                        }
                        return child
                    })
                }


                this.uploadList = newV.map(child => {
                    if (!child.isSuccess) {
                        child.isSuccess = false;
                    }
                    if (!child.isError) {
                        child.isError = false;
                    }
                    return child
                })
            }
        }
    },

    components: {
        tyIcon
    }
}
</script>

<style lang='less' scoped>
@import '../../../css/upload.less';
</style>

7、拖拽

 // 设置拖拽
        setDarg() {
            let uploadRef = this.$refs.uploadRef

            // 在元素内结束拖拽触发
            uploadRef.addEventListener('drop', (e) => {
                e.stopPropagation()
                e.preventDefault()
                // console.log('drop', e.dataTransfer.files);
                // 触发文件上传
                this.selectfilesChange(undefined, e.dataTransfer.files)
                // 触发事件
                this.$emit('handlerDrop', e.dataTransfer.files)
            }, false)

            // 拖拽离开元素时触发
            uploadRef.addEventListener('dragleave', (e) => {
                e.stopPropagation()
                e.preventDefault()
                this.$emit('dropLeave')
            })

            // 拖拽进入元素时触发
            uploadRef.addEventListener('dragenter', (e) => {
                e.stopPropagation()
                e.preventDefault()
                this.$emit('dropEnter')
            })

            // 拖拽在元素内时持续触发
            uploadRef.addEventListener('dragover', (e) => {
                e.stopPropagation()
                e.preventDefault()
                this.$emit('dropChange')
            })

        },

8 可提供的扩展

 props: {
        // 限制上传文件的大小
        size: {
            type: [Number],
            default: 500 * 1024
        },
        // 限制上传文件类型
        accept: {
            type: String,
            default: 'image/*'
        },
        // 是否允许多文件上传
        multiple: {
            type: Boolean,
            default: false
        },
        // 上传成功方法
        uploadSuccess: {
            type: Function
        },
        // 上传失败事件
        uploadError: {
            type: Function
        },
        // 文件校验通过后触发
        acceptAfter: {
            type: Function
        },
        // 移除文件时触发
        fileRemove: {
            type: Function
        },
        // 上传的请求头
        headers: {
            type: Object,
            default: () => { }
        },
        // 是否自动上传文件
        autoUpload: {
            type: Boolean,
            default: true
        },
        // 上传的服务器地址
        action: {
            type: String,
            default: '#'
        },
        // 是否允许cooike
        withCredentials: {
            type: Boolean,
            default: false,
        },
        // 开启预览图
        preView: {
            type: Boolean,
            default: false
        },
        // 携带的参数
        data: {

        },
        // 上传时触发的钩子
        uploadProgress: {
            type: Function
        },
        // 是否展示上传列表
        showList: {
            type: Boolean,
            default: true
        },
        // 开启拖动上传
        drag: {
            type: Boolean,
            default: false
        },
        // 拖拽文件进入是触发
        dropEnter: {
            type: Function
        },
        // 拖拽文件离开时触发
        dropLeave: {
            type: Function
        },
        // 拖拽进入时持续触发
        dropChange: {
            type: Function
        },
        // 拖拽在指定文件中触发
        handlerDrop:{
            type: Function
        }
        ...
  }
© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容