mirror of
https://github.com/keven1024/015.git
synced 2026-05-26 07:08:02 +00:00
refactor(front): replace FileUploadView with a modular file upload process using multiple components for improved maintainability and clarity
This commit is contained in:
28
front/components/Upload/File/FileUploadIndexView.vue
Normal file
28
front/components/Upload/File/FileUploadIndexView.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import VeeForm from '@/components/VeeForm.vue'
|
||||
import FileUploadInputFileView from './FileUploadInputFileView.vue'
|
||||
import FileUploadProgressView from './FileUploadProgressView.vue'
|
||||
import FileUploadResultView from './FileUploadResultView.vue'
|
||||
|
||||
const fileStepList = [
|
||||
{ component: FileUploadInputFileView, key: 'input' },
|
||||
{ component: FileUploadProgressView, key: 'progress' },
|
||||
// { component: FileUploadResultView, key: 'result' },
|
||||
]
|
||||
|
||||
const step = ref('input')
|
||||
|
||||
const renderComponent = computed(() => {
|
||||
return fileStepList.find((item) => item.key === step.value)?.component
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<VeeForm v-slot="{ values }">
|
||||
<div class="rounded-xl p-5 bg-white/50 backdrop-blur-xl w-full lg:w-200">
|
||||
<component :is="renderComponent" :data="values" @change="(key: string) => {
|
||||
step = key
|
||||
}" />
|
||||
</div>
|
||||
</VeeForm>
|
||||
|
||||
</template>
|
||||
38
front/components/Upload/File/FileUploadInputFileView.vue
Normal file
38
front/components/Upload/File/FileUploadInputFileView.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script lang="ts" setup>
|
||||
import showDrawer from '~/lib/showDrawer'
|
||||
import FileShareDrawer from '@/components/Drawer/FileShareDrawer.vue'
|
||||
import FileUploadField from '@/components/Field/FileUploadField.vue'
|
||||
import FormButton from '@/components/Field/FormButton.vue'
|
||||
// const form = useFormContext()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', key: string): void
|
||||
}>()
|
||||
|
||||
const handleFormSubmit = (form: any) => {
|
||||
const { _file } = form?.values || {}
|
||||
showDrawer({
|
||||
render: ({ hide }) => h(FileShareDrawer, {
|
||||
hide, file: _file, onFileHandle: ({ type, config }) => {
|
||||
form.setFieldValue('file_handle_type', type)
|
||||
form.setFieldValue('file', _file)
|
||||
form.setFieldValue('config', config)
|
||||
emit('change', 'progress')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="gap-5 flex flex-col">
|
||||
<div class="text-xl font-normal">上传文件</div>
|
||||
<FileUploadField name="_file" rules="required" />
|
||||
<div class="flex flex-row gap-3">
|
||||
<FormButton @click="handleFormSubmit">
|
||||
<LucideShare class="size-4" />提交
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
149
front/components/Upload/File/FileUploadProgressView.vue
Normal file
149
front/components/Upload/File/FileUploadProgressView.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<script lang="ts" setup>
|
||||
import CircularProgress from '@/components/CircularProgress.vue'
|
||||
import { chunk, shuffle, times } from 'lodash-es';
|
||||
import { cx } from 'class-variance-authority'
|
||||
import calcFileHash from '~/lib/calcFileHash';
|
||||
import { filesize } from 'filesize';
|
||||
const props = defineProps<{
|
||||
data: any
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', key: string): void
|
||||
}>()
|
||||
|
||||
const step = ref<'hash' | 'upload'>('hash')
|
||||
const calcHashTime = ref(0)
|
||||
const chunkSize = ref(0)
|
||||
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))
|
||||
|
||||
useAsyncState(async () => {
|
||||
const { file } = props.data || {}
|
||||
if (!file) return
|
||||
const { size, type } = file || {}
|
||||
const now = Date.now()
|
||||
const hash = await calcFileHash({ file } as any)
|
||||
if (hash) {
|
||||
step.value = 'upload'
|
||||
calcHashTime.value = Date.now() - now
|
||||
}
|
||||
|
||||
const createData = await $fetch<{
|
||||
data: {
|
||||
id: string
|
||||
type: 'init' | 'already'
|
||||
chunk_size: number
|
||||
}
|
||||
}>('/api/file/create', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
size,
|
||||
mime_type: type,
|
||||
hash,
|
||||
}
|
||||
})
|
||||
const { id, chunk_size, type: createType } = createData?.data || {}
|
||||
if (createType !== 'init') {
|
||||
// 文件存在
|
||||
return;
|
||||
}
|
||||
chunkSize.value = chunk_size
|
||||
const chunks = Math.ceil(size / chunk_size);
|
||||
fileSliceUploadStatusList.value = times(chunks, (i) => ({
|
||||
status: 'pending',
|
||||
index: i
|
||||
}))
|
||||
|
||||
const readChunk = (start: number): Promise<ArrayBuffer> => {
|
||||
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 chunkedUploadTasks = chunk(shuffle([...fileSliceUploadStatusList.value]), 3)
|
||||
for (let i = 0; i < chunkedUploadTasks?.length; i++) {
|
||||
await Promise.all(chunkedUploadTasks?.[i]?.map(async (item: any) => {
|
||||
const { index } = item || {}
|
||||
try {
|
||||
const chunk = await readChunk(index * chunk_size);
|
||||
// console.log('chunk', chunk)
|
||||
const formData = new FormData()
|
||||
formData.append('file', new Blob([chunk]))
|
||||
formData.append('index', index + 1)
|
||||
formData.append('id', id)
|
||||
fileSliceUploadStatusList.value[index].status = 'uploading'
|
||||
const res = await $fetch('/api/file/slice', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
const { code } = (res as any) || {}
|
||||
if (code !== 200) {
|
||||
throw new Error('上传失败')
|
||||
}
|
||||
fileSliceUploadStatusList.value[index].status = 'success'
|
||||
} catch (error) {
|
||||
console.log('error', error)
|
||||
// fileSliceStatusList.value[index].status = 'error'
|
||||
}
|
||||
}))
|
||||
}
|
||||
const r = await $fetch<any>('/api/file/finish', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
id
|
||||
}
|
||||
})
|
||||
if (r?.code !== 200) {
|
||||
throw new Error('上传失败')
|
||||
}
|
||||
emit('change', 'result')
|
||||
}, null)
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex flex-col gap-3">
|
||||
<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">
|
||||
<LucideLoaderCircle v-if="step === 'hash'" class="size-5 my-1 animate-spin" />
|
||||
<div v-else>{{ calcHashTime }}ms</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="cx('flex flex-row items-center gap-5 min-w-32', step !== 'upload' && 'opacity-50')">
|
||||
<CircularProgress :size="80" :value="step !== 'upload' ? 0 : uploadProgress" />
|
||||
<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>
|
||||
</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>
|
||||
<div class="flex flex-row flex-wrap gap-1">
|
||||
<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',
|
||||
)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
@@ -1,25 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import VeeForm from '@/components/VeeForm.vue'
|
||||
import FileUploadField from '@/components/Field/FileUploadField.vue'
|
||||
import FormButton from '@/components/Field/FormButton.vue'
|
||||
import showDrawer from '@/lib/showDrawer'
|
||||
import { h } from 'vue'
|
||||
import FileShareDrawer from '@/components/Drawer/FileShareDrawer.vue'
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<VeeForm>
|
||||
<div class="rounded-xl p-5 bg-white/50 backdrop-blur-xl w-full lg:w-200 gap-5 flex flex-col">
|
||||
<div class="text-xl font-normal">上传文件</div>
|
||||
<FileUploadField name="file" rules="required" />
|
||||
<div class="flex flex-row gap-3">
|
||||
<FormButton @click="(form) => {
|
||||
const { file } = form?.values || {}
|
||||
showDrawer({ render: ({ hide }) => h(FileShareDrawer, { hide, file }) })
|
||||
}">
|
||||
<LucideShare class="size-4" />提交
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</VeeForm>
|
||||
</template>
|
||||
@@ -6,7 +6,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FileUploadView from '~/components/Upload/FileUploadView.vue'
|
||||
import FileUploadView from '~/components/Upload/File/FileUploadIndexView.vue'
|
||||
import TextUploadView from '~/components/Upload/TextUploadView.vue'
|
||||
import { isString } from 'lodash-es'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user