使⽤Vue3实现⼀个Upload组件的⽰例代码⽬录
通⽤上传组件开发
我们需要实现如下功能
⾃定义模版
⽀持⽂件上传列表
⽀持⼀系列⽣命周期钩⼦事件,上传事件
拖拽⽀持
写在最后
通⽤上传组件开发
开发上传组件前我们需要了解:
FormData上传⽂件所需API
dragOver⽂件拖拽到区域时触发
dragLeave ⽂件离开拖动区域
drop⽂件移动到有效⽬标时
⾸先实现⼀个最基本的上传流程:
基本上传流程,点击按钮选择,完成上传
代码如下:
<template>
<div class="app-container">
<!--使⽤change事件-->
<input type="file" @change="handleFileChange">
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import axios from 'axios'
export default defineComponent({
name: 'App',
setup() {
const handleFileChange = (e: Event) => {
// 断⾔为HTMLInputElement
const target = e.target as HTMLInputElement
const files = target.files
if(files) {
const uploadedFile = files[0]
const formData = new FormData()
formData.append('file', uploadedFile)
// 使⽤node模拟上传接⼝
axios.post('localhost:3001/upload', formData, {
headers: {
"Content-Type": 'multipart/form-data'
}
}).then(resp=> {
console.log('resp', resp)
}).catch(error=> {
})
}
}
return {
handleFileChange
}
}
})
</script>
<style>
.
page-title {
color: #fff;
}
</style>
结果如下:
到这⾥我们基本的上传已经处理完成了,相对来说还是⽐较简单的,接下来我们创建Uploader.vue⽂件⽤来封装Upload组件。
我们需要实现如下功能
⾃定义模版
我们知道使⽤<input type="file">系统⾃带样式⽐较难看所⽤我们需要如下处理:
对样式进⾏优化,隐藏input
点击<div class="upload-area" @click="triggerUpload"></div>使⽤js出发input的click事件
处理上传状态
代码如下:
<template>
<div class="file-upload">
<div class="upload-area" @click="triggerUpload"></div>
<span v-if="fileStatus==='loading'">正在上传</span>
<span v-else-if="fileStatus==='success'">上传成功</span>
<span v-else-if="fileStatus==='error'">上传失败</span>
<span v-else>点击上传</span>
<input ref="fileInput" type="file" name="file" />
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, reactive, ref } from 'vue'
import axios from 'axios'
type UploadStatus = 'ready' | 'loading' | 'success' | 'error'
export default defineComponent({
props: {
action: { // url地址
type: String,
required: true
}
},
setup(props) {
// input实例对象,通过与ref="fileInput"的关联获取到input实例对象
const fileInput = ref<null | HTMLInputElement>(null)
const fileStatus = ref<UploadStatus>('ready')
/
/ 1.div点击事件
const triggerUpload = () => {
if(fileInput.value) {
fileInput.value.click()
}
}
// 通过div来触发input的change事件
const handleFileChange = (e: Event) => {
const target = e.target as HTMLInputElement
const files = target.files
if(files) {
const uploadedFile = files[0]
const formData = new FormData()
formData.append('file', uploadedFile)
readyFile.status = 'loading' // 上传之前将状态设置为loading
axios.post(props.actions, formData, {
headers: {
"Content-Type": 'multipart/form-data'
}
}).then(resp=> {
console.log('resp', resp)
readyFile.status = 'success' // 上传成功将状态设置为success
}).catch(error=> {
readyFile.status = 'error' // // 上传失败将状态设置为error
})
}
}
return {
fileInput,
triggerUpload,
handleFileChange,
fileStatus
}
}
})
</script>
现在我们已经完成了对上传组件样式的优化,和上传状态的处理,接下来我们需要处理⽂件上传列表⽀持⽂件上传列表
处理⽂件上传列表我们需要有如下实现:
显⽰⽂件名称
状态
可删除
显⽰上传进度
有可能有更丰富的显⽰
在上⼀步代码的基础上做⼀些修改:
<template>
<div class="file-upload">
<div class="upload-area" @click="triggerUpload"></div>
<!-- 点击上传态 -->
<slot v-if="isUploading" name='loading'>
<button disabled>正在上传</button>
</slot>
inputtypefile不上传文件<!-- 上传完毕态 -->
<slot name="uploaded" v-else-if="lastFileData && lastFileData.loaded" :uploadData="lastFileData.data">
<button disabled>点击上传</button>
</slot>
<!-- 默认态 -->
<slot v-else name='default'>
<!-- <button disabled>点击上传</button> -->
<span>点击上传</span>
</slot>
<input ref="fileInput" type="file" name="file" />
<!--展⽰fileList-->
<ul>
<li
v-for="file in filesList"
:key="file.uid"
:
class="`uploaded-file upload-${file.status}`"
>
<span class="filname">
{{ file.name }}
</span>
<button class="delete-icon" @click="reomveFile(file.uid)">Del</button>
</li>
</ul>
</div>
<script lang="ts">
import { last } from 'lodash-es'
import { v4 as uuidv4 } from 'uuid';
// 定义上传状态
type UploadStatus = 'ready' | 'loading' | 'success' | 'error'
// step1 定义上传⽂件对象接⼝类
export interface UploadFile {
uid: string; // ⽂件唯⼀id
size: number; // ⽂件⼤⼩
name: string; // ⽂件名称
status: UploadStatus; // 上传状态
raw: File; // ⽂件
progress?: string; // ⽂件上传进度
resp?: any; // 服务端返回数据
url?: string // 对应展⽰的url
}
export default defineComponent({
props: {
action: { // url地址
type: String,
required: true
}
},
setup(props) {
/
/ input实例对象,通过与ref="fileInput"的关联获取到input实例对象
const fileInput = ref<null | HTMLInputElement>(null)
// step2 上传⽂件列表
const filesList = ref<UploadFile[]>([])
// step4-1 判断是否正在上传中
const isUploading = computed(()=> {
return filesList.value.some((file)=>file.status==='loading')
})
//step4-2 获取上传⽂件的最后⼀项
const lastFileData = computed(()=>{
const lastFile = last(filesList.value)
if(!lastFile) return false
return {
loaded: lastFile?.status === 'success',
data: lastFile?.resp
}
})
// 1.div点击事件
const triggerUpload = () => {
if(fileInput.value) {
fileInput.value.click()
}
}
// 通过div来触发input的change事件
const handleFileChange = (e: Event) => {
const target = e.target as HTMLInputElement
const files = target.files
if(files) {
const uploadedFile = files[0]
const formData = new FormData()
formData.append('file', uploadedFile)
// step3 设置响应式对象,存储到filesList中,以便在页⾯中展⽰
const fileObj = reactive<UploadFile>({
uid: uuid(); // ⽂件唯⼀id
size: uploadedFile.size,
name: uploadedFile.name,
status: 'loading',
raw: uploadedFile
})
filesList.value.push(fileObj)
axios.post(props.actions, formData, {
headers: {
"Content-Type": 'multipart/form-data'
},
/
/step6 处理上传进度
onUploadProgress: (progressEvent)=> {
const complete = (progressEvent.loaded / al * 100 | 0) + '%'
fileObj.progress = complete
}
}).then(resp=> {
console.log('resp', resp)
fileObj.status = 'success'
}).catch(error=> {
fileObj.status = 'error'
}).finally(()=> {
/
/ ⼀张图⽚重复上传时⽆法继续上传bug
if(fileInput.value) {
fileInput.value.value = ''
}
})
}
}
// step7 处理删除
const reomveFile = (uid: string) => {
filesList.value = filesList.value.filter(file=>file.uid!==uid)
}
return {
fileInput,
triggerUpload,
handleFileChange,
fileStatus,
isUploading,
filesList,
lastFileData
}
}
})
</script>
⾸先我们定义上传⽂件对象接⼝类UploadFile
创建了⼀个filesList响应式对象
去掉了fileStatus,因为在UploadFile接⼝中我们已经有定义status
修改模版中的状态,利⽤computed来判读是否是处于上传状态,并增加slot进⾏⾃定义
展⽰上传的图⽚
处理上传进度
处理删除
注意点:如果选择相同的图⽚不会进⾏上传操作,因为我们使⽤的是input的change事件,所以我们需要在上传后将input的value设置为null ⽀持⼀系列⽣命周期钩⼦事件,上传事件
beforeUpload
...
</template>
<script lang="ts">
import { last } from 'lodash-es'
import { v4 as uuidv4 } from 'uuid';
// 定义上传状态
type UploadStatus = 'ready' | 'loading' | 'success' | 'error'
// 定义上传⽂件为boolean或promsie且接受⼀个File
type CheckUpload = ()=> boolean | Promise<File>
export interface UploadFile {
...
}
export default defineComponent({
props: {
action: { // url地址
type: String,
required: true
},
beforeUpload: {
type: Function as PropType<CheckUpload>
}
},
setup(props) {
const fileInput = ref<null | HTMLInputElement>(null)
const filesList = ref<UploadFile[]>([])
// step4-1 判断是否正在上传中
const isUploading = computed(()=> {
return filesList.value.some((file)=>file.status==='loading')
})
const lastFileData = computed(()=>{
const lastFile = last(filesList.value)
if(!lastFile) return false
return {
loaded: lastFile?.status === 'success',
data: lastFile?.resp
}
})
const triggerUpload = () => {
if(fileInput.value) {
fileInput.value.click()
}
}
const handleFileChange = (e: Event) => {
const target = e.target as HTMLInputElement
const files = target.files
const uploadedFile = files[0]
if(props.beforeUpload) {
const result = props.beforeUpload(uploadedFile)
if(result && result instanceof Promise) {
result.then((processFile)=> {
if(uploadedFile instanceof File) {
postFile(uploadedFile)
}
}).catch(error=>{
<(error)
})
} esle if(result===true) {
postFile(uploadedFile)
}
}else{
postFile(uploadedFile)
}
}
const reomveFile = (uid: string) => {
filesList.value = filesList.value.filter(file=>file.uid!==uid)
}
// 处理⽂件上传
const postFile = (readyFile:UploadFile)=> {
const formData = new FormData()
formData.append('file', readyFile.raw)
readyFile.status = 'loading'
axios.post(props.action, formData, {
headers: {
"Content-Type": 'multipart/form-data'
},
onUploadProgress: (progressEvent)=> {
const complete = (progressEvent.loaded / al * 100 | 0) + '%'
// console.log('上传 ' + complete)
readyFile.progress = complete
}
}, ).then(resp=> {
console.log('resp', resp)
// fileStatus.value = 'success'
readyFile.status = 'success'
}).catch(error=> {
// fileStatus.value = 'error'
readyFile.status = 'error'
})
.
finally(()=> {
// ⼀张图⽚重复上传时⽆法继续上传bug
if(fileInput.value) {
fileInput.value.value = ''
}
})
}
return {
fileInput,
triggerUpload,
handleFileChange,
fileStatus,
isUploading,
filesList,
lastFileData
}
}
})
</script>
实现步骤:
在poops处定义属性beforeUpload,同时定义上传⽂件为boolean或promsie且接受⼀个File 将原上传⽅法进⾏封装为postFile
根据beforeUpload返回结果,进⾏接下来的流程处理
onProgress,onSuccess,onError,onChange与之类似
⼤致流程如下图:
dragover和dragleave添加和删除对应的class
drop拿到正在被拖拽的⽂件,删除class,并且触发上传
只有在属性drag为true才能触发
需要注意v-on的使⽤具体可参考
上代码:
<template>
<div class="file-upload">
<div
:class="['upload-area', drag && isDragOver ? 'is-dragover': '' ]"
v-on="events"
>
<!-- 点击上传态 -->
<slot v-if="isUploading" name='loading'>
<button disabled>正在上传</button>
</slot>
<!-- 上传完毕态 -->
<slot name="uploaded" v-else-if="lastFileData && lastFileData.loaded" :uploadData="lastFileData.data"> <button disabled>点击上传</button>
</slot>
<!-- 默认态 -->
<slot v-else name='default'>
<!-- <button disabled>点击上传</button> -->
<span>点击上传</span>
</slot>
</div>
<input ref="fileInput" type="file" name="file" @change="handleFileChange" />
<ul>
<li
v-for="file in filesList"
:key="file.uid"
:class="`uploaded-file upload-${file.status}`"
>
<img
v-if="file.url && listType === 'picture'"
class="upload-list-thumbnail"
:src="file.url"
:alt="file.name"
>
<span class="filname">
{{ file.name }}
</span>
<span class="progress">
{{ file.progress }}
</span>
<button class="delete-icon" @click="reomveFile(file.uid)">Del</button>
</li>
</ul>
</div>
</template>
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论