Skip to content

Commit

Permalink
feat: 优化文件下载,防止预览,增加下载进度显示 (#76)
Browse files Browse the repository at this point in the history
  • Loading branch information
xyqfei authored Jul 4, 2023
1 parent baad9cb commit 236b0bd
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 14 deletions.
2 changes: 2 additions & 0 deletions src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ declare module '@vue/runtime-core' {
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElText: typeof import('element-plus/es')['ElText']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
File: typeof import('./components/RenderMessage/file.vue')['default']
Icon: typeof import('./components/Icon/index.vue')['default']
Expand Down
52 changes: 38 additions & 14 deletions src/components/RenderMessage/file.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<script setup lang="ts">
import { ref, type PropType } from 'vue'
import { type PropType, computed } from 'vue'
import { Close } from '@element-plus/icons-vue'
import { getFileSuffix, formatBytes } from '@/utils'
import type { FileBody } from '@/services/types'
import useDownloadQuenuStore from '@/stores/downloadQuenu'
const { downloadObjMap, download, quenu, cancelDownload } = useDownloadQuenuStore()
const props = defineProps({
body: {
Expand All @@ -10,20 +14,29 @@ const props = defineProps({
},
})
const isDownloading = ref(false)
// 下载文件
const downloadFile = () => {
isDownloading.value = true
const a = document.createElement('a')
a.href = props.body.url
a.download = props.body.fileName
a.target = '_blank'
a.click()
a.remove()
setTimeout(() => {
isDownloading.value = false
}, 500)
// 队列下载
download(props.body.url)
}
const cancelDownloadFile = () => {
cancelDownload(props.body.url)
}
// 目前使用url作为map的key 但是url可能会重复 后面可以考虑使用id 或者 url + id 的形式
const isDownloading = computed(() => {
return downloadObjMap.get(props.body.url)?.isDownloading || false
})
const process = computed(() => {
return downloadObjMap.get(props.body.url)?.process || 0
})
// 是否排队中
const isQuenu = computed(() => {
return quenu.includes(props.body.url)
})
</script>

<template>
Expand All @@ -33,7 +46,18 @@ const downloadFile = () => {
<span class="file-name">{{ body?.fileName || '未知文件' }}</span>
<span class="file-size">{{ formatBytes(body?.size) }}</span>
</div>
<Icon v-if="!isDownloading" icon="xiazai" :size="22" @click="downloadFile" />
<Icon v-else icon="loading" :size="22" spin />
<el-text v-if="isQuenu" class="mx-1" size="small" type="warning" @click="cancelDownloadFile"
>等待下载
<el-icon><Close /></el-icon>
</el-text>
<Icon v-else-if="!isDownloading" icon="xiazai" :size="22" @click="downloadFile" />
<el-progress
v-else
type="circle"
:percentage="process"
:width="22"
:stroke-width="1"
:show-text="false"
/>
</div>
</template>
74 changes: 74 additions & 0 deletions src/hooks/useDownload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { ref } from 'vue'
import { createEventHook } from '@vueuse/core'

const useDownload = () => {
const process = ref(0)
const isDownloading = ref(false)

const { on: onLoaded, trigger } = createEventHook()

const getFileExtension = (url: string) => {
const pathname = new URL(url).pathname
const lastDotIndex = pathname.lastIndexOf('.')
if (lastDotIndex === -1) {
return ''
}
return pathname.slice(lastDotIndex + 1)
}

const getFileName = (url: string) => {
const pathname = new URL(url).pathname
const lastDotIndex = pathname.lastIndexOf('/')
if (lastDotIndex === -1) {
return '未知文件'
}
return pathname.slice(lastDotIndex + 1)
}

const downloadFile = (url: string, filename?: string, extension?: string) => {
isDownloading.value = true
const xhr = new XMLHttpRequest()
xhr.open('GET', url, true)
xhr.responseType = 'blob'
xhr.onprogress = (event) => {
if (event.lengthComputable) {
const percentComplete = Math.floor((event.loaded / event.total) * 100)
process.value = percentComplete
}
}
xhr.onload = () => {
if (xhr.status === 200) {
const blob = new Blob([xhr.response], { type: 'application/octet-stream' })
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
const urlExtension = getFileExtension(url)
const ext = extension || urlExtension
if (filename) {
a.download = `${filename}${ext ? '.' : ''}${ext}`
} else {
const urlFileNmae = getFileName(url)
a.download = urlFileNmae
}
a.click()
// 调用 URL.revokeObjectURL() 方法来释放该内存
URL.revokeObjectURL(a.href)
trigger('success')
} else {
trigger('fail')
}
// 清空进度
process.value = 0
isDownloading.value = false
}
xhr.send()
}

return {
onLoaded,
downloadFile,
process,
isDownloading,
}
}

export default useDownload
84 changes: 84 additions & 0 deletions src/stores/downloadQuenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { reactive, watch } from 'vue'
import { defineStore } from 'pinia'
import useDownload from '@/hooks/useDownload'

type DownloadObjType = {
url: string
isDownloading: boolean
process: number | undefined
}

// 定义一个下载队列的 store
export const useDownloadQuenuStore = defineStore('downloadQuenu', () => {
// 最多可同时执行下载的任务数量
const maxDownloadCount = 1
// 下载队列
const quenu = reactive<string[]>([])
// 下载对象
const downloadObjMap = reactive<Map<string, DownloadObjType>>(new Map())

// 添加到下载队列
const addQuenuAction = (url: string) => {
quenu.push(url)
}

// 从下载队列中移除
const removeQuenuAction = (url: string) => {
const index = quenu.indexOf(url)
if (index > -1) {
quenu.splice(index, 1)
}
}

// 出队列
const dequeue = () => {
if (!quenu.length || downloadObjMap.size >= maxDownloadCount) {
return
}
const url = quenu.shift()
if (url) {
downloadAction(url)
}
}

// 下载
const downloadAction = (url: string) => {
const { downloadFile, isDownloading, process, onLoaded } = useDownload()
const stopWatcher = watch(process, () => {
// 更新下载进度
downloadObjMap.set(url, { url, isDownloading: isDownloading.value, process: process.value })
})
onLoaded(() => {
stopWatcher() // 清除watcher
downloadObjMap.delete(url) // 下载完成后 删除下载对象
dequeue()
})
if (url) {
downloadFile(url)
}
}

const download = (url: string) => {
addQuenuAction(url)
dequeue()
}

// 取消下载
const cancelDownload = (url: string) => {
if (quenu.includes(url)) {
removeQuenuAction(url)
}
}

return {
quenu,
addQuenuAction,
removeQuenuAction,
dequeue,
downloadObjMap,
download,
cancelDownload,
}
})

export default useDownloadQuenuStore

0 comments on commit 236b0bd

Please sign in to comment.