feat: 在文件上传进度视图中添加错误处理和用户提示,优化用户体验

This commit is contained in:
keven
2025-06-14 21:02:05 +08:00
parent d77fc85fd3
commit 28154d09ad

View File

@@ -1,11 +1,11 @@
<script lang="ts" setup>
import CircularProgress from '@/components/CircularProgress.vue'
import { chunk, shuffle, times } from 'lodash-es';
import { chunk, shuffle, times } from 'lodash-es'
import { cx } from 'class-variance-authority'
import calcFileHash from '@/lib/calcFileHash';
import { filesize } from 'filesize';
import calcFileHash from '@/lib/calcFileHash'
import { filesize } from 'filesize'
const props = defineProps<{
data: { file: File, config: any, handle_type: string }
data: { file: File; config: any; handle_type: string }
}>()
const emit = defineEmits<{
@@ -17,19 +17,21 @@ const form = useFormContext()
const step = ref<'hash' | 'upload'>('hash')
const calcHashTime = ref(0)
const chunkSize = ref(0)
const fileSliceUploadStatusList = ref<{
const fileSliceUploadStatusList = ref<
{
status: string
index: number
}[]>([])
}[]
>([])
const successCount = computed(() => fileSliceUploadStatusList.value.filter((item) => item.status === 'success').length)
const alreadyUploadSize = computed(() => successCount.value * chunkSize.value)
const uploadProgress = computed(() => Math.round(alreadyUploadSize.value / (props?.data?.file?.size || 0) * 100))
const uploadProgress = computed(() => Math.round((alreadyUploadSize.value / (props?.data?.file?.size || 0)) * 100))
useAsyncState(async () => {
const { file } = props.data || {}
if (!file) return
const { size, type } = file || {}
const { size, type = 'application/octet-stream' } = file || {}
const now = Date.now()
const hash = await calcFileHash({ file })
if (hash) {
@@ -49,38 +51,39 @@ useAsyncState(async () => {
size,
mime_type: type,
hash,
}
},
})
const { id, chunk_size, type: createType } = createData?.data || {}
if (createType !== 'init') {
// 文件存在
form.setFieldValue('file_id', id)
emit('change', 'result')
return;
return
}
chunkSize.value = chunk_size
const chunks = Math.ceil(size / chunk_size);
const chunks = Math.ceil(size / chunk_size)
fileSliceUploadStatusList.value = times(chunks, (i) => ({
status: 'pending',
index: i
index: i,
}))
const readChunk = (start: number): Promise<ArrayBuffer> => {
const fileReader = new FileReader();
const fileReader = new FileReader()
return new Promise((resolve, reject) => {
const chunk = file.slice(start, start + chunk_size);
fileReader.onload = (e) => resolve(e.target?.result as ArrayBuffer);
fileReader.onerror = reject;
fileReader.readAsArrayBuffer(chunk);
});
};
const chunk = file.slice(start, start + chunk_size)
fileReader.onload = (e) => resolve(e.target?.result as ArrayBuffer)
fileReader.onerror = reject
fileReader.readAsArrayBuffer(chunk)
})
}
const chunkedUploadTasks = chunk(shuffle([...fileSliceUploadStatusList.value]), 3)
for (let i = 0; i < chunkedUploadTasks?.length; i++) {
await Promise.all(chunkedUploadTasks?.[i]?.map(async (item: any) => {
await Promise.all(
chunkedUploadTasks?.[i]?.map(async (item: any) => {
const { index } = item || {}
try {
const chunk = await readChunk(index * chunk_size);
const chunk = await readChunk(index * chunk_size)
// console.log('chunk', chunk)
const formData = new FormData()
formData.append('file', new Blob([chunk]))
@@ -91,7 +94,7 @@ useAsyncState(async () => {
code: number
}>('/api/file/slice', {
method: 'POST',
body: formData
body: formData,
})
const { code } = res || {}
if (code !== 200) {
@@ -102,15 +105,16 @@ useAsyncState(async () => {
console.log('error', error)
// fileSliceStatusList.value[index].status = 'error'
}
}))
})
)
}
const r = await $fetch<{
code: number
}>('/api/file/finish', {
method: 'POST',
body: {
id
}
id,
},
})
if (r?.code !== 200) {
throw new Error('上传失败')
@@ -124,7 +128,6 @@ useAsyncState(async () => {
<div class="text-xl font-normal">正在上传</div>
<div class="flex flex-col items-center gap-4 md:flex-row md:justify-evenly">
<div :class="cx('flex flex-row items-center gap-5', step !== 'hash' && 'opacity-50')">
<div class="flex flex-col gap-0.5 items-center">
<div class="text-xs opacity-50">1.计算hash</div>
<div class="text-3xl font-light">
@@ -138,21 +141,30 @@ useAsyncState(async () => {
<div class="flex flex-col gap-0.5 items-center">
<div class="text-xs opacity-50">2.上传文件</div>
<div class="text-3xl font-light">{{ step !== 'upload' ? 0 : uploadProgress }}%</div>
<div class="text-sm opacity-50" v-if="alreadyUploadSize">{{ filesize(alreadyUploadSize) }} / {{ filesize(data?.file?.size) }}</div>
<div class="text-sm opacity-50" v-if="alreadyUploadSize">
{{ filesize(alreadyUploadSize) }} / {{ filesize(data?.file?.size) }}
</div>
</div>
</div>
</div>
<div class="flex flex-row gap-2 items-baseline">
<div class="text-md font-normal">详细信息</div>
<div class="text-xs opacity-50" v-if="step === 'upload' && fileSliceUploadStatusList?.length > 0">当前正在上传分块{{ `${successCount}/${Math.ceil(data?.file?.size / chunkSize)}` }} 并发:3</div>
<div class="text-xs opacity-50" v-if="step === 'upload' && fileSliceUploadStatusList?.length > 0">
当前正在上传分块{{ `${successCount}/${Math.ceil(data?.file?.size / chunkSize)}` }} 并发:3
</div>
</div>
<div class="flex flex-row flex-wrap gap-1">
<div v-for="i in fileSliceUploadStatusList" :class="cx('rounded size-4 ',
<div
v-for="i in fileSliceUploadStatusList"
:class="
cx(
'rounded size-4 ',
i.status === 'pending' && 'bg-white/90',
i.status === 'uploading' && 'bg-yellow-500',
i.status === 'success' && 'bg-green-500',
)" />
i.status === 'success' && 'bg-green-500'
)
"
/>
</div>
</div>
</template>