前端实现分片上传

273 阅读8分钟

前言

公司代码重构,一些技术需要调整,就比如上传下载功能。上个版本使用的是华为云的obs ,因为某些原因后端架构调整,导致前端也要进行改变,经讨论使用MiniIo和OnlyOffice。

基本思路

  1. 点击上传文件拿到文件大小
  2. 根据分片大小把文件进行分割拿到分片数量
  3. 请求创建文件分片接口,拿到后端给的分片信息和单个文件上传的地址
  4. 请求每个单个文件上传地址
  5. 上传完成每个单个文件地址后请求后端合并上传接口通知他已经上传完成。

代码实现-完整代码在最下方

<template>
    <div>
        <el-upload action="#" :before-upload="handleBeforeUpload" :http-request="handleHttpRequest" :show-file-list="false" :multiple="multiple" :drag="drag">
            <el-button type="primary">点击上传</el-button>
        </el-upload>
        <!-- 上传文件的列表 -->
        <div class="fileBox" v-if="showFile">
            <div v-for="task in uploadTaskList" :key="task.uid" class="fileItem">
                <div class="rowBox">
                    <div class="name" @click.stop="openPreview(task)">{{ task.fileName }}</div>
                    <div class="delBox">
                        <i class="el-icon-close" @click.stop="handleRemove(task.uid)"></i>
                    </div>
                </div>
                <el-progress :percentage="task.totalPercent" :stroke-width="2" style="width: 100%;" v-if="task.totalPercent < 100" />
            </div>
        </div>
    </div>
</template>

分析

使用element ui上传的手动上传功能来实现,节省开发时间。上面主要代码为before-upload和http-request

上传前的操作:调用element ui的before-upload上传前的钩子 这里为了控制上传文件类型、上传大小、最大个数,实现方式如下

        // 上传前回调
        handleBeforeUpload(file) {
            if (this.uploadTaskList.length >= this.limit) {
                this.$message.error(`最多上传${this.limit}个文件`)
                return false
            }
            let fileSize = file.size / 1024 / 1024
            // 文件大小限制
            if (fileSize > this.sizeLimit) {
                this.$message.error(`文件大小不能超过${this.sizeLimit}M`)
                return false
            }
            // 分片大小 默认 5M
            this.chunkSize = this.sliceSize * 1024 * 1024
            return true
        }

其中uploadTaskList上传任务列表即上传文件列表 limit最大个数 sizeLimit为文件大小限制

手动上传

调用elementui的http-request方法实现手动上传,这里就要做上面基本思路的操作了

  // 上传
        async handleHttpRequest(options) {
            // 获取文件
            const file = options.file
            // 获取分片数量
            const chunksCount = Math.ceil(file.size / this.chunkSize)
            // 创建上传任务
            let task = {
                index: this.uploadTaskList.length,
                uid: file.uid,
                // 文件信息
                file: file,
                fileName: file.name,
                fileUrl: '',
                size: file.size,
                // 获取文件大小
                totalSize: file.size,
                // 获取分片数量
                chunksCount: chunksCount,
                // 已上传的总字节数
                totalUploaded: 0,
                // 进度相关:记录每个分片已上传的字节数
                uploadedSizePerChunk: new Array(chunksCount).fill(0),
                // 上传进度
                totalPercent: 0,
                // 创建控制器
                controller: new AbortController(),
                // 分片地址列表
                chunkUrlList: [],
                // 上传文件id
                uploadId: '',
                // 上传文件参数
                filePrefix: '',
                // 上传文件名称
                finalName: '',
                // 标记正在上传
                isUploading: true
            }
            this.uploadTaskList.push(task)
            // 更新进度的方法
            const updateProgress = () => {
                const percent = Math.ceil((task.totalUploaded / task.totalSize) * 100)
                task.totalPercent = percent
                // el-upload 会监听这个
                options.onProgress({ percent: percent });
            };
            // 获取-创建分片上传的参数
            let params = {
                bucketName: 'files',
                chunkSize: chunksCount,
                fileName: file.name
            }
            // 创建分片上传
            try {
                const initRes = await createMultipartUpload(params)
                if (initRes.code == 500) {
                    this.$message.error(initRes.msg)
                    return
                }
                // 获取创建分片的参数
                task.uploadId = initRes.uploadId
                task.filePrefix = initRes.filePrefix
                task.finalName = initRes.finalName
                // 获取分片地址列表并排序
                task.chunkUrlList = []
                for (let i = 0; i < task.chunksCount; i++) {
                    const key = `chunk_${i}`
                    if (!initRes[key]) {
                        throw new Error(`缺少第 ${i + 1} 个分片上传地址`)
                    }
                    task.chunkUrlList[i] = initRes[key]
                }
                // ---------------------------------------------------开始分片上传-------------------------------------------------
                // 并发上传所有分片
                const uploadPromises = []
                for (let i = 0; i < task.chunksCount; i++) {
                    // 获取分片开始位置
                    const start = i * this.chunkSize
                    // 获取分片结束位置
                    const end = Math.min(start + this.chunkSize, task.totalSize)
                    // 分割文件
                    const chunkBlob = file.slice(start, end)
                    // 分片上传
                    const promise = this.uploadChunk(task.chunkUrlList[i], chunkBlob, task.controller.signal, (progressEvent) => {
                        // 计算这个分片上传了多少
                        const chunkUploaded = progressEvent.loaded;
                        // 更新“该分片”上传量(防止重复叠加)
                        task.totalUploaded = task.totalUploaded - task.uploadedSizePerChunk[i] + chunkUploaded;
                        task.uploadedSizePerChunk[i] = chunkUploaded;
                        // 更新进度条
                        updateProgress();
                    })
                    uploadPromises.push(promise)
                }
                // 等待所有分片完成
                await Promise.all(uploadPromises)
                const mergeData = {
                    bucketName: 'files',
                    objectName: task.filePrefix + task.finalName,
                    uploadId: task.uploadId,
                }
                // 合并分片完成上传
                const res = await completeMultipartUpload(mergeData)
                if (res.code == 200) {
                    task.fileUrl = res.data.uploadUrl
                    // this.$message.success(`${file.name} 上传成功!`)
                    // 通知 el-upload 上传成功
                    options.onSuccess(res);
                } else {
                    console.log(res, '-------------res');
                    this.$message.error(`${file.name} 合并失败:${res.msg}`)
                    options.onError(new Error(res.msg));
                }

            } catch (error) {
                console.log(error, '-------------error', task);
                let index = this.uploadTaskList.findIndex(v => v.uid === task.uid)
                if (index > -1) {
                    this.uploadTaskList.splice(index, 1)
                }
                if (error.name === 'AbortError' || error.message.toLowerCase().includes('canceled') || error.code === 'ERR_CANCELED') {
                    this.$message.info('用户取消上传')
                } else {
                    this.$message.error('上传失败')
                    options.onError(error)
                }
            } finally {
                task.isUploading = false
                // 检查是否所有任务都已完成
                this.$nextTick(() => {
                    this.checkAllUploaded()
                })
            }
        }
         // 上传单个分片
        uploadChunk(url, fileBlob, signal, onUploadProgress) {
            return axios.put(url, fileBlob, {
                signal: signal,
                // 设置请求进度
                onUploadProgress
            })
        }
         // 检查是否所有文件都上传完成
        checkAllUploaded() {
            const allFinished = this.uploadTaskList.every(task => !task.isUploading)
            if (allFinished && this.uploadTaskList.length) {
                this.handleAfterAllUploads()
            }
        }

//  所有文件上传完成后的回调
        handleAfterAllUploads() {
            // console.log('上传完成', this.uploadTaskList);
            this.$emit('success', this.uploadTaskList)
        }
          // 删除文件
        handleRemove(uid) {
            const index = this.uploadTaskList.findIndex(v => v.uid === uid)
            if (index !== -1) {
                const task = this.uploadTaskList[index]
                if (task.isUploading) {
                    task.controller.abort()
                }
                this.uploadTaskList.splice(index, 1)
            }
        },
        // 预览
        openPreview(val) {
            if (val.fileUrl) {
                previewShowView(val.fileUrl)
            }
        }

完整代码如下

<template>
    <div>
        <el-upload action="#" :before-upload="handleBeforeUpload" :http-request="handleHttpRequest" :show-file-list="false" :multiple="multiple" :drag="drag">
            <el-button type="primary">点击上传</el-button>
        </el-upload>
        <!-- 列表 -->
        <div class="fileBox" v-if="showFile">
            <div v-for="task in uploadTaskList" :key="task.uid" class="fileItem">
                <div class="rowBox">
                    <div class="name" @click.stop="openPreview(task)">{{ task.fileName }}</div>
                    <div class="delBox">
                        <i class="el-icon-close" @click.stop="handleRemove(task.uid)"></i>
                    </div>
                </div>
                <el-progress :percentage="task.totalPercent" :stroke-width="2" style="width: 100%;" v-if="task.totalPercent < 100" />
            </div>
        </div>
    </div>
</template>

<script>
import axios from 'axios'
import { createMultipartUpload, completeMultipartUpload } from '@/api/common'
import { previewShowView } from '@/utils/previewShowView.js'
export default {
    props: {
        // 是否支持多选文件
        multiple: {
            type: Boolean,
            default: false
        },
        // 最大允许上传个数
        limit: {
            type: Number,
            default: 5
        },
        // 限制大小默认10M
        sizeLimit: {
            type: Number,
            default: 300
        },
        // 分片大小
        sliceSize: {
            type: Number,
            default: 5
        },
        // 是否支持拖拽上传
        drag: {
            type: Boolean,
            default: false
        },
        // 文件类型
        fileList: {
            type: Array,
            default: () => []
        },
        // 是否显示上传后的数据
        showFile: {
            type: Boolean,
            default: true,
        },
    },
    data() {
        return {
            // 分片大小 
            chunkSize: 0,
            // 上传任务列表
            uploadTaskList: []
        }
    },
    watch: {
        fileList: {
            handler(val) {
                this.uploadTaskList = val.map(v => {
                    let uid = v.uid || Date.now() + Math.random()
                    return {
                        ...v,
                        isUploading: false,
                        uid: uid
                    }
                })
            },
            immediate: true
        }

    },
    methods: {
        // 上传前回调
        handleBeforeUpload(file) {
            if (this.uploadTaskList.length >= this.limit) {
                this.$message.error(`最多上传${this.limit}个文件`)
                return false
            }
            let fileSize = file.size / 1024 / 1024
            // 文件大小限制
            if (fileSize > this.sizeLimit) {
                this.$message.error(`文件大小不能超过${this.sizeLimit}M`)
                return false
            }
            // 分片大小 默认 5M
            this.chunkSize = this.sliceSize * 1024 * 1024
            return true
        },
        // 上传
        async handleHttpRequest(options) {
            // 获取文件
            const file = options.file
            // 获取分片数量
            const chunksCount = Math.ceil(file.size / this.chunkSize)
            // 创建上传任务
            let task = {
                index: this.uploadTaskList.length,
                uid: file.uid,
                // 文件信息
                file: file,
                fileName: file.name,
                fileUrl: '',
                size: file.size,
                // 获取文件大小
                totalSize: file.size,
                // 获取分片数量
                chunksCount: chunksCount,
                // 已上传的总字节数
                totalUploaded: 0,
                // 进度相关:记录每个分片已上传的字节数
                uploadedSizePerChunk: new Array(chunksCount).fill(0),
                // 上传进度
                totalPercent: 0,
                // 创建控制器
                controller: new AbortController(),
                // 分片地址列表
                chunkUrlList: [],
                // 上传文件id
                uploadId: '',
                // 上传文件参数
                filePrefix: '',
                // 上传文件名称
                finalName: '',
                // 标记正在上传
                isUploading: true
            }
            this.uploadTaskList.push(task)
            // 更新进度的方法
            const updateProgress = () => {
                const percent = Math.ceil((task.totalUploaded / task.totalSize) * 100)
                task.totalPercent = percent
                // el-upload 会监听这个
                options.onProgress({ percent: percent });
            };
            // 获取-创建分片上传的参数
            let params = {
                bucketName: 'files',
                chunkSize: chunksCount,
                fileName: file.name
            }
            // 创建分片上传
            try {
                const initRes = await createMultipartUpload(params)
                if (initRes.code == 500) {
                    this.$message.error(initRes.msg)
                    return
                }
                // 获取创建分片的参数
                task.uploadId = initRes.uploadId
                task.filePrefix = initRes.filePrefix
                task.finalName = initRes.finalName
                // 获取分片地址列表并排序
                task.chunkUrlList = []
                for (let i = 0; i < task.chunksCount; i++) {
                    const key = `chunk_${i}`
                    if (!initRes[key]) {
                        throw new Error(`缺少第 ${i + 1} 个分片上传地址`)
                    }
                    task.chunkUrlList[i] = initRes[key]
                }
                // ---------------------------------------------------开始分片上传-------------------------------------------------
                // 并发上传所有分片
                const uploadPromises = []
                for (let i = 0; i < task.chunksCount; i++) {
                    // 获取分片开始位置
                    const start = i * this.chunkSize
                    // 获取分片结束位置
                    const end = Math.min(start + this.chunkSize, task.totalSize)
                    // 分割文件
                    const chunkBlob = file.slice(start, end)
                    // 分片上传
                    const promise = this.uploadChunk(task.chunkUrlList[i], chunkBlob, task.controller.signal, (progressEvent) => {
                        // 计算这个分片上传了多少
                        const chunkUploaded = progressEvent.loaded;
                        // 更新“该分片”上传量(防止重复叠加)
                        task.totalUploaded = task.totalUploaded - task.uploadedSizePerChunk[i] + chunkUploaded;
                        task.uploadedSizePerChunk[i] = chunkUploaded;
                        // 更新进度条
                        updateProgress();
                    })
                    uploadPromises.push(promise)
                }
                // 等待所有分片完成
                await Promise.all(uploadPromises)
                const mergeData = {
                    bucketName: 'files',
                    objectName: task.filePrefix + task.finalName,
                    uploadId: task.uploadId,
                }
                // 合并分片完成上传
                const res = await completeMultipartUpload(mergeData)
                if (res.code == 200) {
                    task.fileUrl = res.data.uploadUrl
                    // this.$message.success(`${file.name} 上传成功!`)
                    // 通知 el-upload 上传成功
                    options.onSuccess(res);
                } else {
                    console.log(res, '-------------res');
                    this.$message.error(`${file.name} 合并失败:${res.msg}`)
                    options.onError(new Error(res.msg));
                }

            } catch (error) {
                console.log(error, '-------------error', task);
                let index = this.uploadTaskList.findIndex(v => v.uid === task.uid)
                if (index > -1) {
                    this.uploadTaskList.splice(index, 1)
                }
                if (error.name === 'AbortError' || error.message.toLowerCase().includes('canceled') || error.code === 'ERR_CANCELED') {
                    this.$message.info('用户取消上传')
                } else {
                    this.$message.error('上传失败')
                    options.onError(error)
                }
            } finally {
                task.isUploading = false
                // 检查是否所有任务都已完成
                this.$nextTick(() => {
                    this.checkAllUploaded()
                })
            }
        },
        // 上传单个分片
        uploadChunk(url, fileBlob, signal, onUploadProgress) {
            return axios.put(url, fileBlob, {
                signal: signal,
                // 设置请求进度
                onUploadProgress
            })
        },
        // 检查是否所有文件都上传完成
        checkAllUploaded() {
            const allFinished = this.uploadTaskList.every(task => !task.isUploading)
            if (allFinished && this.uploadTaskList.length) {
                this.handleAfterAllUploads()
            }
        },
        //  所有文件上传完成后的回调
        handleAfterAllUploads() {
            // console.log('上传完成', this.uploadTaskList);
            this.$emit('success', this.uploadTaskList)
        },
        // 删除文件
        handleRemove(uid) {
            const index = this.uploadTaskList.findIndex(v => v.uid === uid)
            if (index !== -1) {
                const task = this.uploadTaskList[index]
                if (task.isUploading) {
                    task.controller.abort()
                }
                this.uploadTaskList.splice(index, 1)
            }
        },
        // 预览
        openPreview(val) {
            if (val.fileUrl) {
                previewShowView(val.fileUrl)
            }
        }
    }
}
</script>

<style lang="scss" scoped>
.fileBox {
    margin-top: 10px;
    .fileItem {
        margin-top: 5px;
        .rowBox {
            position: relative;
            cursor: pointer;
            display: flex;
            justify-content: space-between;
            align-items: center;
            &:hover {
                background-color: #f5f7fa;
                .name {
                    color: #409eff;
                }
                .delBox {
                    opacity: 1;
                }
            }
            .name {
                color: #606266;
                display: block;
                margin-right: 40px;
                overflow: hidden;
                padding-left: 4px;
                text-overflow: ellipsis;
                transition: color 0.3s;
                white-space: nowrap;
                text-align: left;
            }
            .delBox {
                cursor: pointer;
                opacity: 0;
                color: #606266;
            }
        }
    }
}
</style>

js 封装版

import { createMultipartUpload, completeMultipartUpload } from '@/request/upload.js'
import { Message } from 'element-ui'
import axios from 'axios'
const minIo = {
    // 上传文件列表
    uploadTaskList: [],
    // 分片大小 默认5MB
    chunkSize: 5,
    // 上传文件
    uploadFile: uploadFile,
    // 新增:根据 uid 安全移除任务
    removeTask(uid) {
        const index = this.uploadTaskList.findIndex(task => task.uid === uid)
        if (index !== -1) {
            this.uploadTaskList.splice(index, 1)
        }
    },
}

// 上传文件
async function uploadFile(options, bucketName = 'files', callBack, progressChange, chunkSize) {
    // 获取文件
    let file = options.file

    // 获取分片大小
    let fileChunkSize = chunkSize || this.chunkSize;
    fileChunkSize = fileChunkSize * 1024 * 1024
    // 获取分片数量
    const chunksCount = Math.ceil(file.size / fileChunkSize);
    let task = {
        // 任务id
        uid: file.uid,
        // 任务索引
        index: this.uploadTaskList.length,
        // 文件
        file: file,
        // 文件名称
        fileName: file.name,
        // 文件大小
        fileSize: file.size,
        // 上传进度
        progress: 0,
        // 已上传的总字节数
        totalUploaded: 0,
        // 进度相关:记录每个分片已上传的字节数
        uploadedSizePerChunk: new Array(chunksCount).fill(0),
        // 分片大小
        chunkSize: fileChunkSize,
        // 分片数量
        chunksCount: chunksCount,
        // 分片地址列表
        chunkUrlList: [],
        // -------------------------参数
        // 上传文件id
        uploadId: '',
        // 上传文件参数
        filePrefix: '',
        // 上传文件名称
        finalName: '',
        // 标记正在上传
        isUploading: true
    }
    this.uploadTaskList.push(task)
    // 更新进度的方法
    const updateProgress = () => {
        const percent = Math.ceil((task.totalUploaded / task.fileSize) * 100)
        task.progress = percent
        progressChange(task, task.uid, percent)
    };
    // 获取-创建分片上传的参数
    let params = {
        bucketName: bucketName,
        chunkSize: chunksCount,
        fileName: file.name
    }
    // 创建分片上传
    try {
        const initRes = await createMultipartUpload(params)
        if (initRes.code == 500) {
            Message.error(initRes.msg || '初始化分片上传失败')
            this.removeTask(task.uid)
            return
        }
        // 获取创建分片的参数
        task.uploadId = initRes.uploadId
        task.filePrefix = initRes.filePrefix
        task.finalName = initRes.finalName
        // 获取分片地址列表并排序
        task.chunkUrlList = []
        for (let i = 0; i < task.chunksCount; i++) {
            const key = `chunk_${i}`
            if (!initRes[key]) {
                throw new Error(`缺少第 ${i + 1} 个分片上传地址`)
            }
            task.chunkUrlList[i] = initRes[key]
        }
        // ---------------------------------------------------开始分片上传-------------------------------------------------
        // 并发上传所有分片
        const uploadPromises = []
        // 分割文件
        for (let i = 0; i < task.chunksCount; i++) {
            // 获取分片文件开始位置
            const start = i * task.chunkSize
            // 获取分片文件结束位置
            const end = Math.min(start + task.chunkSize, task.fileSize)
            // 分割文件 
            const chunkBlob = file.slice(start, end)
            // 分片上传
            const promise = uploadChunk(task.chunkUrlList[i], chunkBlob, (e) => {
                // 计算这个分片上传了多少
                const chunkUploaded = e.loaded;
                // 更新“该分片”上传量(防止重复叠加)
                task.totalUploaded = task.totalUploaded - task.uploadedSizePerChunk[i] + chunkUploaded;
                task.uploadedSizePerChunk[i] = chunkUploaded;
                // 更新进度条
                updateProgress();
            })
            uploadPromises.push(promise)
        }
        // 等待所有分片完成
        await Promise.all(uploadPromises)
        const mergeData = {
            bucketName: bucketName,
            objectName: task.filePrefix + task.finalName,
            uploadId: task.uploadId,
        }
        // 合并分片完成上传
        try {
            const res = await completeMultipartUpload(mergeData)
            if (res.code == 0) {
                task.fileUrl = res.data.uploadUrl
                // 上传成功,移除任务
                this.removeTask(task.uid)
                // 回调单个文件结果
                callBack(task)
            } else {
                Message.error(`${file.name} 合并失败:${res.msg}`)
                this.removeTask(task.uid)
                options.onError(new Error(res.msg));
            }
        } catch (error) {
            Message.error(error?.response?.data?.message || '合并失败')
        }

    } catch (error) {
        this.removeTask(task.uid)
        Message.error(`上传失败,请联系管理员`)
        if (typeof options.onError === 'function') {
            options.onError(error);
        }
    } finally {
        task.isUploading = false
    }
}

// 上传单个分片
function uploadChunk(url, fileBlob, onUploadProgress) {
    return axios.put(url, fileBlob, {
        // 设置请求进度
        onUploadProgress,
        headers: {
            isToken: true
        }
    })
}
export default minIo;

js 封装版使用

  MinIo.uploadFile(options, this.bucketName, (taskFile) => {
                // 单个文件上传完成后触发
                let index = this.fileList.findIndex(v => v.fileUrl == taskFile.fileUrl)
                if (index == -1) {
                    this.fileList.push({
                        fileUrl: taskFile.fileUrl,
                        fileName: taskFile.fileName,
                        fileSize: taskFile.fileSize
                    })
                }
                this.$emit('fileList', this.fileList)
            }, (task, uid, progress) => {
                // 进度条
                let item = this.UploadingList.find(v => v.uid == uid)
                if (item) {
                    item.loadingNumber = progress
                } else {
                    this.UploadingList.push({
                        loadingNumber: progress,
                        uid: uid,
                        name: task.file.name
                    })
                }
            }, this.chunkSize)

previewView.js

import { showView } from '@/request/upload.js'
import { Message } from 'element-ui'
import { baseUrl } from '@/utils/config'
import router from '@/router'
// 下载附件
export const downLoadFileFn = (fileUrl, fileName = '') => {
    let url = `${baseUrl}file/downFile?fileUrl=${fileUrl}&fileName=${fileName}`
    window.open(url, '_blank')
}


/**
 * 预览
 *  
 * 使用方法:
 *      import { previewFnView, downLoadFileFn } from '@/utils/previewFnView.js'
 * 
 *      previewFnView(url) ---> 外部跳转 返回一个promise
 *      previewFnView(url,false,id) ---> 内部跳转 返回一个promise  id-dom元素id
 *      previewFnView(url,false,id,false) ---> 内部跳转 返回一个promise  id-dom元素id 在不支持预览的文件类型时,不提示
 * 
 * @param {*} url - 预览地址
 * @param {*} isJump - 是否跳转到新页面
 * @param {*} id - 预览组件id
 * @param {*} isMsg - 是否提示
 * @returns 
 */
export const previewFnView = (url, isJump = true, id = 'previewIdViewId', isMsg = true) => {
    return new Promise(async (resolve, reject) => {
        let suffix = url.substring(url.lastIndexOf('.') + 1).toLowerCase()
        let returnMessage = {
            code: 0,
            suffix: suffix,
            url: url
        }
        // 白名单
        let whitelist = ['MP4', 'mp4', 'png', 'jpg', 'jpeg', 'gif', 'pdf', 'doc', 'docx', 'xlsx', 'xls', 'pptx', 'ppt']
        if (!whitelist.includes(suffix)) {
            if (isMsg) {
                Message.error('暂不支持该文件类型预览')
            }
            returnMessage.code = 500
            // return resolve(returnMessage)
            return reject(returnMessage)
        }
        let isFlag = ['MP4', 'mp4', 'png', 'jpg', 'jpeg', 'gif', 'pdf'].includes(suffix)
        if (isJump && !isFlag) {
            let path = router.resolve({
                path: '/preview',
                query: {
                    url: url
                }
            })
            window.open(path.href, '_blank')
            return
        }
        await loadScript('http://192.168.0.174/web-apps/apps/api/documents/api.js')
        let params = {
            fileUrl: url
        }
        showView(params).then((res) => {

            if (res.code == 0) {
                let pData = res.data
                if (isFlag) {
                    if (isJump) {
                        window.open(pData.showViewUrl, '_blank')
                    }
                    returnMessage.url = pData.showViewUrl
                    resolve(returnMessage)
                } else {
                    const config = {
                        document: {
                            isForm: true,
                            key: pData.documentKey,
                            // 文件类型
                            fileType: pData.fileType, // 展示的文件名称
                            title: pData.title, // 需要预览的url(这里是我服务器中的一个文件地址)
                            url: pData.proxyUrl,
                            permissions: {
                                download: false,
                                edit: false,
                                comment: false,
                                chat: false,
                                copy: true,
                                deleteCommentAuthorOnly: false,
                                editCommentAuthorOnly: false,
                                modifyContentControl: false,
                                modifyFilter: false,
                                print: false,
                                review: false,
                            }
                        },
                        editorConfig: {
                            user: {
                                name: "匿名用户",
                                id: "guest-1"
                            },
                            customization: {
                                // showMenu: false,      // 隐藏顶部菜单(File/View/Plugins)
                                showLeftPanel: true, // 隐藏左侧边栏(搜索/评论等图标)
                                //showToolbar: false,    // 保留工具栏(如需隐藏也设为false)
                                //showStatusBar: false,  // 隐藏底部状态栏
                                compactToolbar: true,
                                leftMenu: true,
                                rightMenu: true,
                                hideRightMenu: true, // hide or show right panel on first loading
                                toolbar: true,
                                statusBar: true,
                                help: false,
                                compactHeader: true,
                                toolbarNoTabs: true,
                                toolbarHideFileName: false,
                            },
                            lang: "zh",
                            mode: "view",
                        },
                        documentType: pData.documentType, // Word文档类型('cell'=Excel, 'slide'=PPT, 'text'=WORD)
                        type: "desktop",
                        width: '100%', // 预览区域的高度
                        height: '100%',
                        token: pData.token, // 由后端生成
                    };
                    const docEditor = new DocsAPI.DocEditor(id, config);
                    resolve(returnMessage)
                }
            } else {
                returnMessage.code = 500
                reject(returnMessage)
            }
        })
    })
}

// 动态加载 OnlyOffice API 脚本
const loadScript = (src) => {
    return new Promise((resolve, reject) => {
        if (window.DocsAPI) {
            resolve(window.DocsAPI);
            return;
        }
        const script = document.createElement('script');
        script.type = 'text/javascript';
        script.src = src;
        script.onload = () => resolve(window.DocsAPI);
        script.onerror = () => reject(new Error('Failed to load OnlyOffice API'));
        document.head.appendChild(script);
    });
}