课程名称:web前端架构师
课程章节:第11周 第五章
主讲老师:张轩
课程内容:完成上传组件-支持拖拽上传 代码重构
拖拽上传
分析
拖拽区域触发事件:
- 拖拽元素进入区域时触发dragenter;
- 拖拽元素进入区域后悬停触发dragover;
- 拖拽元素离开区域时触发dragleave;
- 拖拽元素放置时触发 drop, 此时我们可以发送 上传文件请求
在拖拽元素进入区域悬停时,我们可以给拖拽区域元素一个 class is-draging
,当离开拖拽区域时,就移除 is-draging
这个class。
下面编写触发拖拽事件的代码
// 拖拽元素进入区域或离开区域时触发。
// over为true 为进入区域悬停,false 时为离开拖拽区域
function handleDrag (e: DragEvent, over:boolean) {
e.preventDefault()
e.stopPropagation()
dragOver.value = over
}
// 拽元素放置时触发
function handleDrop (e: DragEvent) {
e.preventDefault()
e.stopPropagation()
// is
if (e.dataTransfer) {
// 发送请求
uploadFile(e.dataTransfer.files)
}
dragOver.value = false
}
编写代码实现
首先我们在使用组件时,需要用户传递一个参数 drag ,是否需要拖拽上传
interface Props{
beforeUpload?: (file: File) => boolean | Promise<boolean>
drap?: boolean
name?: string
}
事件处理, 如果需要 拖拽上传,添加拖拽事件
let events: {[key: string]:(e: any) => void} = {
click: triggerUpload
}
if (props.isDrag) {
events = {
...events,
dragover: (e: DragEvent) => { handleDrag(e, true) },
dragleave: (e: DragEvent) => { handleDrag(e, false) },
drop: handleDrop
}
}
给元素绑定事件和class, 事件绑定通过 v-on 绑定多个事件
<div
class="choose-flle"
:class="{ 'is-draging': drag && dragOver }"
v-on="events"
>
...
</div>
编写测试代码
it('test drag upload', async () => {
const wrapper = mount(UploadFile, {
props: {
drag: true
}
})
const uploadFileEl = wrapper.get('.choose-file')
await uploadFileEl.trigger('dragover')
expect(uploadFileEl.classes()).toContain('is-draging')
await uploadFileEl.trigger('dragleave')
expect(uploadFileEl.classes()).not.toContain('is-draging')
await uploadFileEl.trigger('drop', {
dataTransfer: {
files: [testFile]
}
})
request.mockResolvedValueOnce({ status: 'success' })
expect(request).toHaveBeenCalled()
await flushPromises()
expect(wrapper.findAll('li').length).toBe(1)
})
这里需要注意过是触发 drop 事件. https://test-utils.vuejs.org/api/#trigger
trigger 方法第一个参数是事件名称,第二个参数需要传递的参数
查看下我们需要传递的参数格式
function handleDrop (e: DragEvent) {
e.preventDefault()
e.stopPropagation()
// is
if (e.dataTransfer) {
uploadFile(e.dataTransfer.files)
}
dragOver.value = false
}
知道需要传递的参数格式就可以触发 drop 事件了
const testFile = new File(['foo'], 'foo.txt', {
type: 'text/plain'
})
await uploadFileEl.trigger('drop', {
dataTransfer: {
files: [testFile]
}
})
代码重构
有时候我们不需要选择完图片后自动上传,这个就需要将我们之前上传文件给提出来
之前上传文件的代码
async function uploadFile (files: FileList) {
const uploadFile = files[0]
const formData = new FormData()
formData.append(props.name, uploadFile)
const fileObj = reactive<FileItem>({
id: '' + Date.now(),
name: uploadFile.name,
size: uploadFile.size,
status: 'loading',
raw: uploadFile
})
try {
fileObj.status = 'loading'
loadStatus.value = 'loading'
if (props.beforeUpload) {
const res = await props.beforeUpload(uploadFile)
if (!res) {
return
}
}
uploadFiles.push(fileObj)
const res = await request<{code: number}>(formData)
fileObj.status = 'success'
loadStatus.value = 'success'
if (res.code === 0) {
console.log('上传成功')
}
} catch (e) {
fileObj.status = 'fail'
loadStatus.value = 'fail'
} finally {
(fileRef.value as HTMLInputElement).value = ''
}
}
上传文件的步骤
- 准备
- 执行 beforeUpload 钩子,是否需要往下执行
- 将文件添加到 fileList 中
- 判断是否需要自动上传
- 需要就执行文件上传
- 不需要什么也不用做
- 给外部暴露一个手动上传文件的方法
<script setup lang="ts">
import { reactive, ref, computed } from 'vue'
import request from '@/utils/request'
type UploadStatus = 'ready' | 'loading' | 'success' | 'fail'
interface FileItem {
id: string
name: string
size: number
status:UploadStatus
raw: File
res?: any
}
interface Props{
beforeUpload?: (file: File) => boolean | Promise<boolean>
drag?: boolean
name?: string
autoUpload?: boolean
}
const fileRef = ref<HTMLInputElement | null>(null)
const dragOver = ref(false)
const props = withDefaults(defineProps<Props>(), {
drag: false,
beforeUpload: () => true,
name: 'file',
autoUpload: false
})
// const uploadStatus = ref<UploadStatus>('ready')
const uploadFileList = reactive<FileItem[]>([])
const isUploading = computed(() => uploadFileList.some(file => file.status === 'loading'))
const loadStatus = ref<UploadStatus>('ready')
async function handleChangeFile (e: Event) {
const target = e.target as HTMLInputElement
const files = target.files
if (files) {
uploadReady(files[0])
}
}
function addFileToList (file: File) {
if (!file) return
const fileObj = reactive<FileItem>({
id: '' + Date.now(),
name: file.name,
size: file.size,
status: 'ready',
raw: file
})
uploadFileList.push(fileObj)
if (props.autoUpload) {
uploadFile(file, fileObj)
}
}
async function uploadReady (file: File) {
if (props.beforeUpload) {
const res = await props.beforeUpload(file)
if (!res) {
return false
}
}
addFileToList(file)
}
// 给外部暴露一个手动上传文件的方法
function uploadFiles () {
uploadFileList.filter(file => file.status !== 'success').forEach(file => uploadFile(file.raw, file))
}
defineExpose({
uploadFiles
})
async function uploadFile (file: File, fileObj: FileItem) {
const formData = new FormData()
formData.append(props.name, file)
try {
loadStatus.value = fileObj.status = 'loading'
const res = await request<{code: number}>(formData)
fileObj.status = loadStatus.value = 'success'
if (res.code === 0) {
console.log('上传成功')
}
} catch (e) {
fileObj.status = loadStatus.value = 'fail'
} finally {
(fileRef.value as HTMLInputElement).value = ''
}
}
function handleDrag (e: DragEvent, over:boolean) {
e.preventDefault()
e.stopPropagation()
dragOver.value = over
}
function handleDrop (e: DragEvent) {
e.preventDefault()
e.stopPropagation()
if (e.dataTransfer) {
uploadReady(e.dataTransfer.files[0])
}
dragOver.value = false
}
let events: {[key: string]:(e: any) => void} = {
click: triggerUpload
}
if (props.drag) {
events = {
...events,
dragover: (e: DragEvent) => { handleDrag(e, true) },
dragleave: (e: DragEvent) => { handleDrag(e, false) },
drop: handleDrop
}
}
function triggerUpload () {
fileRef.value?.click()
}
function delFile (index: number) {
uploadFileList.splice(index, 1)
}
</script>
<template>
<div
class="choose-file"
:class="{ 'is-draging': drag && dragOver }"
v-on="events"
>
<input
type="file"
name="file"
ref="fileRef"
:style="{display: 'none'}"
@change="handleChangeFile"
>
<div>
<slot
v-if="isUploading"
name="loading"
>
<button>正在上传</button>
</slot>
<slot
v-else-if="loadStatus==='success'"
name="uploaded"
:uploaded-data="{status: loadStatus}"
>
<button>点击上传</button>
</slot>
<slot v-else>
<button>点击上传</button>
</slot>
</div>
</div>
<ul>
<li
v-for="(file,index) in uploadFileList"
:key="file.id"
:class="'upload-'+ file.status"
>
<span class="filename">{{ file.name }}</span>
<button
class="delete-icon"
@click="delFile(index)"
>
del
</button>
</li>
</ul>
</template>