一个基于uniapp的图片视频上传组件

1,069 阅读4分钟

1、实现功能

  • 图片、视频上传
  • 相机、相册选择
  • 上传loading
  • 上传中断
  • 图片、视频预览
  • 文件删除
  • 错误提示
  • 文件体积限制

image.png

2、待增加

  • 切片上传

3、代码

前提

没怎么开发过小程序,在写一个基于uniapp开发一个很基础的表单业务的时候,产品说要图片和视频都要可以预览,可以拍照、拍摄、相册等方式,所以写的时候跌跌撞撞,开发了这么一个可复用的组件,权当自用。有什么问题,大家可以一起完成。

模板

<view class="upload-file">
        <!-- 标题区域 -->
        <view class="upload-title">
            <text>附件上传({{ maxSize ? `单个视频/图片不超过${maxSizeShowText}MB` : '支持视频/图片' }})</text>
        </view>
        <!-- 上传区域 -->
        <view class="upload-area">
            <view class="upload-item" v-for="(item, index) in preViewFileList" :key="index" :class="item.status">
                <!-- 上传加载状态 -->
                <image v-if="item.status == 'loading'" class="uploader-img" :class="item.status" src="~@/static/images/loading.svg" mode="scaleToFill" @tap="exampleAction(item)"></image>
                <!-- 上传失败状态 -->
                <!-- <image v-else-if="item.status == 'fail'" class="uploader-img" :class="item.status" src="~@/static/images/fail.svg" mode="scaleToFill" @tap="exampleAction(item)"></image> -->
                <!-- 图片 -->
                <image v-else-if="item.type == 'image'" class="uploader-img" :src="item.localSrc" @tap="exampleAction(item)" mode="scaleToFill"></image>
                <!-- 视频 -->
                <video v-else-if="item.type == 'video'" class="uploader-video" :src="item.localSrc" @tap="exampleAction(item)" object-fit="fill" :show-center-play-btn="false" :controls="false"></video>
            </view>

            <view class="upload-item" @tap="chooseVideoImage" v-if="uploadButtonShow()">
                <view class="upload-button">
                    <image src="~@/static/images/uploadButton.svg" mode="scaleToFill" />
                </view>
            </view>
        </view>
        <!-- 预览区域 -->
        <view class="upload-preview" v-if="previewShow">
            <!-- 关闭按钮 -->
            <view class="closeBtn" @tap="closePreview">×</view>
            <!-- 图片预览 -->
            <image class="uploade-image" v-if="previewFileData.type == 'image'" :src="previewFileData.localSrc" mode="aspectFit"></image>
            <!-- 视频预览 -->
            <video class="uploade-video" v-if="previewFileData.type == 'video'" :src="previewFileData.localSrc" object-fit="contain"></video>
        </view>
        <!-- 结果示例点击操作菜单 -->
        <u-action-sheet class="actionSheet" :list="actionSheetList" @click="actionSheetClick" v-model="actionSheetShow" border-radius="24" :safe-area-inset-bottom="true"></u-action-sheet>
    </view>

样式


@keyframes loading {
    from {
        -webkit-transform: rotate(0deg);
    }

    to {
        -webkit-transform: rotate(360deg);
    }
}

.upload-file {
    width: 750rpx;
    // border: 1px solid red;
    padding: 32rpx 32rpx 31rpx 32rpx;

    // 标题
    .upload-title {
        font-size: 28rpx;
        color: rgba(14, 19, 25, 0.90);
        line-height: 40rpx;
        font-weight: 500;
    }

    // 上传区域
    .upload-area {
        margin-top: 33rpx;
        display: flex;
        flex-wrap: wrap;

        .upload-item {
            width: 72rpx;
            height: 72rpx;
            margin: 0 18rpx 18rpx 0;
            border-radius: 8rpx;
            overflow: hidden;
            display: flex;
            justify-content: center;
            align-items: center;

            &.loading,
            &.fail {
                background-color: rgba(0, 0, 0, .3);
            }

            image,
            video {
                width: 100%;
                height: 100%;

                &.loading {
                    width: 60%;
                    height: 60%;
                    animation: loading 1.5s linear infinite;
                }

                &.fail {
                    width: 60%;
                    height: 60%;
                }
            }

            .upload-button {
                width: 100%;
                height: 100%;

            }
        }
    }

    .upload-preview {
        padding: 20rpx;
        position: fixed;
        top: 0;
        left: 0;
        width: 100vw;
        height: 100vh;
        background-color: #000;
        color: #fff;
        display: flex;
        justify-content: center;
        align-items: center;
        // 这个值是不是很大?我不想做动态计算了o(╥﹏╥)o,后面的小伙伴你们有更合适的方法来处理下吧
        z-index: 99999999999999999;

        .closeBtn {
            position: absolute;
            right: 30rpx;
            top: 30rpx;
            z-index: 999;
            width: 50rpx;
            height: 50rpx;
            line-height: 42rpx;
            text-align: center;
            border: 4rpx solid #fff;
            border-radius: 50%;
            font-size: 42rpx;
        }

        .uploade-image {
            width: 100%;
            height: 100%;
        }

        .uploade-video {
            width: 100%;
            height: 100%;
        }
    }

}

逻辑

export default {
    components: {
        // ActionSheet
    },
    props: {
        // 上传结果
        value: {
            type: Array,
            default: [],
        },
        // 上传地址
        actionUrl: {
            type: String,
            required: true,
        },
        // 上传请求方式
        actionMethod: {
            type: String,
            default: "POST",
        },
        // 最大上传图片、视频数量,默认值为9
        maxNumber: {
            type: [String, Number],
        },
        // 文件大小限制
        maxSize: {
            type: Number,
        }
    },
    watch: {
        // 监听上传文件列表数据改变
        fileList: {
            immediate: true,
            handler(newVal) {
                this.$emit("input", newVal.filter(item => item.status == 'success').map(({ name, file }) => ({ name, file })))
            }
        }
    },
    computed: {
        // 剩余可上传的图片、视频数量
        remainder() {
            if (Number(this.maxNumber)) {
                return (Number(this.maxNumber) ? Number(this.maxNumber) : 9) - this.fileList.length
            } else {
                return 9
            }
        },
        // 预览文件列表
        preViewFileList() {
            return this.fileList.filter(item => item.status != 'fail')
        },
        // 文件大小限制提示数字
        maxSizeShowText() {
            return this.maxSize / 1024 / 1024 > 1 ? (parseInt(this.maxSize / 1024 / 1024)) : (this.maxSize / 1024 / 1024).toFixed(2);
        }
    },
    data() {
        return {
            // 文件列表
            fileList: [],
            // 控制是否显示
            previewShow: false,
            // 当前预览对象
            previewFileData: {},
            // 操作菜单显示状态
            actionSheetShow: false,
            // 操作菜单配置
            successActionSheetList: [
                {
                    disabled: true,
                    text: '附件操作',
                    color: 'blue',
                    fontSize: 28,
                    color: '#999999',
                    fontWeight: 400,
                    lineHeight: 108,
                },
                {
                    text: '预览',
                    fontFamily: " PingFangSC-Medium",
                    fontSize: 32,
                    color: 'rgba(14,19,25,0.90)',
                    fontWeight: 500,
                },
                {
                    text: '删除',
                    fontFamily: " PingFangSC-Medium",
                    fontSize: 32,
                    color: '#FF5C5C',
                    fontWeight: 500,
                }
            ],
            failActionSheetList: [
                {
                    disabled: true,
                    text: '附件操作',
                    color: 'blue',
                    fontSize: 28,
                    color: '#999999',
                    fontWeight: 400,
                    lineHeight: 108,
                },
                {
                    text: '删除',
                    fontFamily: " PingFangSC-Medium",
                    fontSize: 32,
                    color: '#FF5C5C',
                    fontWeight: 500,
                }
            ],
            actionSheetList: [],

        }
    },
    methods: {
        // 选择图片、视频交互按钮
        chooseVideoImage() {
            uni.showActionSheet({
                title: "选择上传类型",
                itemList: ['图片', '视频'],
                success: (res) => {
                    console.log(res)
                    if (res.tapIndex == 0) {
                        this.chooseImages()
                    } else {
                        this.chooseVideo()
                    }
                }
            })
        },
        // 上传图片
        chooseImages() {
            uni.chooseImage({
                // 选择文件个数,默认9
                count: this.remainder,
                // 相册、相机
                sourceType: ['album', 'camera'],
                success: async (res) => {
                    console.log("选择图片成功:", res);
                    // 如果限制了文件大小,则只上传满足大小限制的图片
                    let filePaths = res.tempFiles.filter(({ size }) => this.maxSize ? size <= this.maxSize : true).map(({ path }) => path);
                    if (res.tempFilePaths.length - filePaths.length > 0) {
                        uni.showToast({
                            icon: "none",
                            title: `${res.tempFilePaths.length - filePaths.length}张图片超过${this.maxSizeShowText}MB,无法上传!`
                        })
                    }

                    // 此次上传任务列表
                    let pArr = [];
                    filePaths.forEach((filePath, index) => {
                        pArr.push(new Promise((resolve, reject) => {
                            let newFileIndex = this.fileList.length
                            let newFileData = {
                                status: 'loading',
                                type: 'image',
                                id: Math.random(),
                                uploadTask: {},
                            }
                            console.log('准备上传图片:URL', this.actionUrl);
                            newFileData.uploadTask = uni.uploadFile({
                                url: this.actionUrl,
                                method: this.actionMethod,
                                filePath: filePath,
                                name: 'file',
                                header: {
                                    'apikey': this.$store.state.userInfo.apikey,
                                    'session': this.$store.state.userInfo.session,
                                },
                                success: (res) => {
                                    let data = JSON.parse(res.data);
                                    if (data.code == 200) {
                                        console.log('上传图片中介', data);
                                        let fileInfo = data.data
                                        if (fileInfo.ret == '200') {
                                            // 上传成功
                                            console.log('上传图片成功', fileInfo);
                                            this.$set(this.fileList, newFileIndex, {
                                                localSrc: filePath,
                                                file: fileInfo.data.url,
                                                name: fileInfo.data.file,
                                                status: 'success',
                                                type: 'image',
                                                id: Math.random(),
                                            })
                                            // 上传成功:返回正确状态
                                            resolve({ [index]: 'success' });
                                        } else {
                                            // 上传失败
                                            console.log('上传图片失败', fileInfo);
                                            this.$set(this.fileList, newFileIndex, {
                                                status: 'fail',
                                                type: 'image',
                                            })
                                            // 上传失败:返回失败状态
                                            resolve({ [index]: 'fail' });
                                        }
                                    } else {
                                        // 上传失败
                                        console.log('上传图片失败', data);
                                        this.$set(this.fileList, newFileIndex, {
                                            status: 'fail',
                                            type: 'image',
                                        })
                                        // 上传失败:返回失败状态
                                        resolve({ [index]: 'fail' });
                                    }
                                },
                                fail: (res) => {
                                    if (res.errMsg.includes('abort')) {
                                        // 取消上传
                                        console.log('取消图片视频', res);
                                        let targetIndex = this.fileList.findIndex(item => item.id == this.previewFileData.id)
                                        this.fileList.splice(targetIndex, 1)
                                        // 取消上传:返回取消状态
                                        resolve({ [index]: 'abort' });
                                    } else {
                                        // 上传失败
                                        console.log('上传图片失败', res);
                                        this.$set(this.fileList, newFileIndex, {
                                            status: 'fail',
                                            type: 'image',
                                        })
                                        // 上传失败:返回失败状态
                                        resolve({ [index]: 'fail' });
                                    }
                                },
                            })
                            this.fileList.push(newFileData)
                        }))
                    })
                    let r = await Promise.all(pArr);
                    if (Array.isArray(r)) {
                        let successCount = r.filter(item => {
                            for (const key in item) {
                                return item[key] == 'fail'
                            }
                        })
                        if (successCount.length > 0) {
                            uni.showToast({
                                icon: "none",
                                title: `${successCount.length}张图片上传失败!`
                            })
                        }
                    }
                },
                fail: (res) => {
                    this.flag = '失败'
                    this.res = res;
                }
            });
        },
        // 上传视频
        chooseVideo() {
            uni.chooseVideo({
                // 相册、相机
                sourceType: ['album', 'camera'],
                compressed: false,
                success: (res) => {
                    console.log("选择视频成功:", res);
                    if (this.maxSize && res.size > this.maxSize) {
                        uni.showToast({
                            icon: "none",
                            title: `视频大小不能超过${this.maxSizeShowText}MB!`
                        })
                        return;
                    }
                    let filePath = res.tempFilePath;
                    let newFileIndex = this.fileList.length
                    let newFileData = {
                        status: 'loading',
                        type: 'video',
                        id: Math.random(),
                        uploadTask: {},
                    }
                    console.log('准备上传视频:URL', this.actionUrl);
                    newFileData.uploadTask = uni.uploadFile({
                        url: this.actionUrl,
                        method: this.actionMethod,
                        filePath: filePath,
                        name: 'file',
                        header: {
                            'apikey': this.$store.state.userInfo.apikey,
                            'session': this.$store.state.userInfo.session,
                        },
                        success: (res) => {
                            let data = JSON.parse(res.data);
                            if (data.code == 200) {
                                console.log('上传视频中介', data);
                                let fileInfo = data.data
                                if (fileInfo.ret == '200') {
                                    // 上传成功
                                    console.log('上传视频成功', fileInfo);
                                    this.$set(this.fileList, newFileIndex, {
                                        localSrc: filePath,
                                        file: fileInfo.data.url,
                                        name: fileInfo.data.file,
                                        status: 'success',
                                        type: 'video',
                                        id: Math.random(),
                                    })
                                    return
                                } else {
                                    // 上传失败
                                    console.log('上传视频失败', fileInfo);
                                    this.$set(this.fileList, newFileIndex, {
                                        status: 'fail',
                                        type: 'video',
                                    })
                                }
                            } else {
                                // 上传失败
                                console.log('上传视频失败', data);
                                this.$set(this.fileList, newFileIndex, {
                                    status: 'fail',
                                    type: 'video',
                                })
                            }
                            // 提示上传失败
                            uni.showToast({
                                icon: "none",
                                title: `视频上传失败!`
                            })
                        },
                        fail: (res) => {
                            if (res.errMsg.includes('abort')) {
                                // 取消上传
                                console.log('取消上传视频', res);
                                let targetIndex = this.fileList.findIndex(item => item.id == this.previewFileData.id)
                                this.fileList.splice(targetIndex, 1)
                            } else {
                                // 上传失败
                                console.log('上传视频失败', res);
                                this.$set(this.fileList, newFileIndex, {
                                    status: 'fail',
                                    type: 'video',
                                })
                                // 提示上传失败
                                uni.showToast({
                                    icon: "none",
                                    title: `视频上传失败!`
                                })
                            }
                        },
                    })
                    this.fileList.push(newFileData)
                },
                fail: (res) => {
                    this.flag = '失败'
                    this.res = res;
                }
            })
        },
        // 点击结果示例
        exampleAction(file) {
            // 根据文件状态显示,显示不同的操作菜单
            this.actionSheetList = file.status == 'success' ? this.successActionSheetList : this.failActionSheetList
            // 显示操作菜单
            this.actionSheetShow = true
            // 当前操作的文件
            this.previewFileData = file
        },
        // 预览文件
        previewFile() {
            console.log("触发了预览");
            this.previewShow = true;
        },
        // 关闭预览
        closePreview() {
            this.previewShow = false;
        },
        // 控制是否显示上传按钮
        uploadButtonShow() {
            if (Number(this.maxNumber)) {
                return this.fileList.length < Number(this.maxNumber)
            }
            return true;
        },
        // 点击操作菜单按钮
        actionSheetClick(index) {
            console.log(`点击了操作菜单`, index, this.actionSheetList[index].text)
            let text = this.actionSheetList[index].text;
            if (text == '预览') {
                this.previewFile()
            } else if (text == '删除') {
                if (this.previewFileData.status == 'loading') {
                    // 如果是正在上传中,则取消上传
                    this.previewFileData.uploadTask?.abort && this.previewFileData.uploadTask.abort()
                }
                // 如果不是正在上传中(失败、成功)则删除该数据
                let targetIndex = this.fileList.findIndex(item => item.id == this.previewFileData.id)
                this.fileList.splice(targetIndex, 1)
            }
        },
    }

}

组件内文件列表数据格式

[
    {
        localSrc: 'https://v1.uviewui.com/index/banner_1920x1080.png',
        name: 'file1',
        file: 'https://v1.uviewui.com/index/banner_1920x1080.png',
        type: 'image',
        status: "success",
        id: Math.random(),
    },
    {
        localSrc: 'https://v1.uviewui.com/index/banner_1920x1080.png',
        type: 'image',
        name: 'file2',
        file: 'https://v1.uviewui.com/index/banner_1920x1080.png',
        status: "loading",
        id: Math.random(),
    },
]

输出格式

[
    {
        name: 'file1',
        file: 'https://v1.uviewui.com/index/banner_1920x1080.png',
    },
    {
        name: 'file2',
        file: 'https://v1.uviewui.com/index/banner_1920x1080.png',
    },
]

4、问题解析

1)错误提示

当选择多张图片时,会出现部分图片上传失败的情况,需要统计失败的数量、并通知用户,所以这里采用了Promise的all来处理

// 此次上传任务列表
let pArr = [];
filePaths.forEach((filePath, index) => {
    pArr.push(new Promise((resolve, reject) => {
        uni.uploadFile({
            url: this.actionUrl,
            method: this.actionMethod,
            filePath: filePath,
            name: 'file',
            success: (res) => {
                // 上传成功:返回正确状态
                resolve({ [index]: 'success' });
            },
            fail: (res) => {
                // 上传失败:返回失败状态
                resolve({ [index]: 'fail' });
            },
        })
        this.fileList.push(newFileData)
    }))
})
let r = await Promise.all(pArr);
if (Array.isArray(r)) {
    let successCount = r.filter(item => {
        for (const key in item) {
            return item[key] == 'fail'
        }
    })
    if (successCount.length > 0) {
        uni.showToast({
            icon: "none",
            title: `${successCount.length}张图片上传失败!`
        })
    }
}

2)交互差异

正在上传的文件只能删除不能预览、上传成功的文件可以删除和预览,所以需要有字段表示上传的状态

image.png 在不同的情况下更新状态值

image.png

image.png

在模板中根据状态值不同做调整

image.png 另:保存了上传失败的结果,但是这里没有在模板中渲染出来,大家可以根据自己的需求看看要不要呈现上传失败的效果

image.png

image.png