Redian新闻
>
前端文件上传的几种交互造轮子

前端文件上传的几种交互造轮子

公众号新闻

来源 | OSCHINA 社区

作者 | 京东云开发者-京东物流 刘海鼎

原文链接:https://my.oschina.net/u/4090830/blog/10084832

背景

前端文件上传本来是一个常规交互操作,没什么特殊性可言,但是最近在做文件上传,需要实现截图粘贴上传,去找了下有没有什么好用的组件,网上提供的方法有,但是没找完整的组件来支持 cv 上传,经过了解发现可以用剪贴板功能让自己的 cv 实现文件上传,于是自己就整合了目前几种文件上传的交互方式,码了一个支持 cv 的 vue3 文件上传组件(造个轮子)。

介绍

作为一个完整的组件内容还是挺多的,这里主要介绍下上传交互中一些主要功能,包括上传的几种交互方式,
上传进度的获取,上传类型的限制,默认上传请求和自定义上传请求。
以下代码都是非完整代码,大家用于参考实现过程,可以通过以下代码修改来完成自己想要的交互功能。

几种交互

1,点击选择上传
点击选择是最常见的上传交互,之前原生上传控件,样式修改比较麻烦,为了修改上传样式,我们可以把该控件设置隐藏,用其他元素通过从 click 交互,来触发该文件选择控件。在选择文件控件上绑定 onchange 事件,该控件在 change 后获取到文件,然后调用上传方法,实现如下:
<div class="uploader-content" @click="handleClick">
<input ref="inputRef"
class="uploader-target"
:name="name" :multiple="multiple"
:accept="accept" type="file"
@change="handleChange" />

</div>

<script setup>
const inputRef = shallowRef(null)
const handleClick = () => {
inputRef.value.value = ''
inputRef.value?.click()
}
const handleChange = (e) => {
const files = e.target.files
if (!files) return
// 获取到文件后调用附件上传方法
uploadFiles(files)
}
</script>

<style lang='less' scoped>
.uploader-target {
display: none;
}
</style>

2,拖动上传
拖拽文件上传,首先在页面上建立一个拖放区域,在拖放区域上绑定拖放事件,监听拖放事件 drop 内容中 datTransfer 中是包含 files, 如果存在 files,获取 files 然后调用上传附件方法。
拖放区域可以通过事件 dragover 来检查拖放文件是否进入拖放区域来设置拖放区域悬浮样式,通过 dragleave 来检查离开拖放区取消悬浮样式。
进行交互提示
实现如下:
<div class="uploader-drag" v-if="props.uploadMode == 'drag'" :class="['dragger', dragover ? 'dragover' : '']" @drop.prevent="onDrop" @dragover.prevent="onDragover"
@dragleave.prevent="dragover = false">
<div class="dragicon-box">
<span>+</span>
</div>

</div>
<script setup>
const dragover = ref(false)
const onDrop = (e) => {
const files = Array.from(e.dataTransfer?.files)
dragover.value = false
uploadFiles(files);
}
const onDragover = () => {
dragover.value = true
}
</script>

3,复制上传(复制检测区域设置)
复制上传的交互步骤
・将文件保存到剪贴板:执行键盘快捷键或者使用鼠标复制
・将鼠标移动到可粘贴区: 判断是否移动到可粘贴区,来确定是否在执行粘贴后上传,否则整个页面都会作为粘贴区,
・执行粘贴操作:执行键盘粘贴快捷键(ctrl+v)
粘贴区绑定 paste 事件,在触发 paste 事件前将鼠标移到粘贴区,复制会被检查不在粘贴区,阻止上传操作,实现如下:
<div class="uploader-paste" 
v-if="props.uploadMode == 'paste'"
:class="['dragger', dragover ? '' : '']"
@mouseover.stop="clipboardover = true"
@mouseleave.stop="clipboardover = false"
@drop.prevent="onDrop"
@dragover.prevent="onDragover"
@dragleave.prevent="dragover = false"
@paste="pasteFun"
>
<!--默认插槽内容-->
<template v-if="$slots.default == null">
<div class="dragicon-box">
<span>+</span>
</div>
</template>
<slot />
</div>

<script setup>
const clipboardover = ref(false)
const pasteFun = (e) => {
if(!clipboardover.value) return
const clipboardFile = e.clipboardData.files;
uploadFiles(clipboardFile)
}
</script>

上传模式

根据以上三种交互,大家可自由组合上传形式,比如点击和拖拽,拖拽和粘贴组合等等,我这边目前按点击,拖拽,粘贴叠加组合,设置为:

・点击上传,click

・拖拽上传 drag(包括点击上传和拖拽上传)

・粘贴上传 paste (包括点击,拖拽和复制上传)

通过传参 uploadeMode 设置 (click, drag, paste)

组件设置:

<div class="uploader-content" @click="handleClick">
<input
ref="inputRef"
class="uploader-target"
:name="name"
:multiple="multiple"
:accept="props.accept"
type="file"
@change="handleChange"
v-if="props.uploadMode != 'click'"
/>

<!-- click -->
<div class="uploader-click" v-if="props.uploadMode == 'click'">
<slot />
<input
ref="inputRef"
class="uploader-target"
:name="name"
:multiple="multiple"
:accept="accept"
type="file"
@change="handleChange"
@click.stop />

</div>
<!-- drag -->
<div class="uploader-drag"
v-if="props.uploadMode == 'drag'"
:class="['dragger', dragover ? 'dragover' : '']"
@drop.prevent="onDrop"
@dragover.prevent="onDragover"
@dragleave.prevent="dragover = false">

<template v-if="$slots.default == null">
<div class="dragicon-box">
<span>+</span>
</div>
</template>
<slot />
</div>
<!-- copy -->
<div class="uploader-paste"
v-if="props.uploadMode == 'paste'"
:class="['dragger', dragover ? '' : '']"
@mouseover.stop="clipboardover = true"
@mouseleave.stop="clipboardover = false"
@drop.prevent="onDrop"
@dragover.prevent="onDragover"
@dragleave.prevent="dragover = false"
@paste="pasteFun"
>

<template v-if="$slots.default == null">
<div class="dragicon-box">
<span>+</span>
</div>
</template>
<slot />
</div>
</div>
</template>

组件应用

<Upload action="https://jsonplaceholder.typicode.com/posts/" uploadMode="click">
<div>点击上传</div>
</Upload>
<script lang="ts">
import Upload from '@/components/uploader';
</script>

文件限制

文件限制包括是否多文件上传限制 multiple, 上传数量 limit 限制,上传类型 accept 限制,这些设置参考了 element-plus 上传组件,在其基础上做了简化。实现如下
multiple 和 accept 首先需要在点击控件上绑定,以便于在点击选择上传时就能够过滤对应文件,拖拽上传和粘贴上传,无法通过 input [type=file] 组件控制需要在上传方法中判断过滤,(以粘贴上传为例)
组件实现
<div class="uploader-content" @click="handleClick">
<input ref="inputRef"
class="uploader-target"
:name="name" :multiple="multiple" :accept="props.accept" type="file"
@change="handleChange" v-if="props.uploadMode != 'click'" @click.stop />


<div class="uploader-paste" v-if="props.uploadMode == 'paste'" :class="['dragger', dragover ? '' : '']"
@mouseover.stop="clipboardover = true"
@mouseleave.stop="clipboardover = false"
@drop.prevent="onDrop"
@dragover.prevent="onDragover"
@dragleave.prevent="dragover = false"
@paste="pasteFun"
>

<template v-if="$slots.default == null">
<div class="dragicon-box">
<span>+</span>
</div>
</template>
<slot />
</div>
</div>

<script setup>
import { shallowRef, ref } from 'vue';
const inputRef = shallowRef(null)
// 上传文件
const uploadFiles = (files) => {
if (files.length === 0) return
const { limit, multiple, accept } = props
// 是否多文件限制,主要用于拖拽和粘贴上传中
if (!multiple) {
files = Array.from(files).slice(0, 1)
}
// 文件数量
if (limit && files.length > limit) {
/*具体大家需要的逻辑可自行定义*/
return
}
// 文件类型限制
if (accept) {
files = filesFiltered(Array.from(files), accept)
}
//在文件符合条件后执行上传方法
}
// 文件过滤
const filesFiltered = (files, accept) => {
return files.filter((file) => {
const { type, name } = file
const extension = name.includes('.') ? `.${name.split('.').pop()}` : ''
const baseType = type.replace(//.*$/, '')
return accept
.split(',')
.map((type) => type.trim())
.filter((type) => type)
.some((acceptedType) => {
if (acceptedType.startsWith('.')) {
return extension === acceptedType
}
if (//*$/.test(acceptedType)) {
return baseType === acceptedType.replace(//*$/, '')
}
if (/^[^/]+/[^/]+$/.test(acceptedType)) {
type === acceptedType
}
return false
})
})
}

</script>

上传进度设置

获取文件上传进度,使用 ajax 中的 progress 事件监听机制,回传数据 loaded 进度,和 ttotal 进行计算,获取到计算的百分比通过 process 插槽线上在界面上。
具体实现如下:
组件实现
文件限制后执行组件上传,默认情况下走内置的上传方法,如果做了自定义,上传进度也需要自己实现(自己实现过程可以参考内置方法中的实现)
// 上传方法调用
ajaxUpload({...props, file})
// 上传方法实现
ajaxUpload = (options) => {
const xhr = new XMLHttpRequest()
const action = option.action
console.log(xhr, xhr.upload)
if (xhr.upload) {
// 建立progress监听
xhr.upload.addEventListener('progress', (evt:any) => {
const progressEvt = evt
progressEvt.percent = evt.total > 0 ? (evt.loaded / evt.total) * 100 : 0
// 回传进度数据
option.onProgress(progressEvt)
})
}
}

同样文件上传成功,异常等方法也可以通过监听 load 并且判断 xhr.status 来实现,

xhr.addEventListener('load', () => {
if (xhr.status < 200 || xhr.status >= 300) {
return option.onError(getError(action, option, xhr))
}
option.onSuccess(getBody(xhr))
})
组件使用
・配置获取进度数据回调函数 onProgress
・配置接收回传的进度数据进行赋值
・配置进度条插槽显示进度数据
<Upload action="https://jsonplaceholder.typicode.com/posts/" :limit="3" uploadMode="click" :onProgress="progress">
<div class="button">点击上传</div>
<template v-slot:progress>
<!-自定义的进度条样式,大家可以根据自己的想象,自行设置进度条样式-->
<div class="progress-box">
<div class="progress">
<span class="line" :style="{'width': progressval + '%'}"></span>
</div>
<span class="val">{{progressval}} %</span>
</div>
</template>
</Upload>
<script setup>
import {ref} from 'vue'
import Upload from '@/components/uploader';
const progressval = ref(0)
const progress = (evt)=>{
progressval.value = evt.percent.toFixed(2)
},
// 上传成功
const uploadSucess = (e)=>{
console.log('sucess', e)
}
// 上传异常
const uploadError= (e)=> {
console.log('sucess', e)
}
</script>

自定义上传请求
默认情况下,不需要自定义上传请求,组件内置了上传请求,如果个人有需求可以自定义上传请求,子定义上传请求,是在文件限制流程后,检查是否有自定义请求方法,如果存在就将文件传入自定义请求方法。
组件实现:
// 上传文件
const uploadFiles = (files) => {
if (files.length === 0) return
const { limit, multiple, accept, httpRequest } = props
// 是否多文件限制,主要用于拖拽和粘贴上传中
if (!multiple) {
files = Array.from(files).slice(0, 1)
}
// 文件数量
if (limit && files.length > limit) {
/*具体大家需要的逻辑可自行定义*/
return
}
// 文件类型限制
if (accept) {
files = filesFiltered(Array.from(files), accept)
}
//在文件符合条件后执行上传方法
// 自定义上传方法调用
if(httpRequest) {
return httpRequest(files)
}
}
组件应用:
注意点: 通过自定义上传方法实现时,在原来组件上的属性 action 无效
<Upload :limit="3" uploadMode="click" :onProgress="progress" :onSuccess="uploadSucess" :onError="uploadError" :httpRequest="httpRequest">
<div class="button">点击上传</div>
<template v-slot:progress>
<div class="progress-box">
<div class="progress">
<span class="line" :style="{'width': progressval + '%'}"></span>
</div>
<span class="val">{{progressval}} %</span>
</div>
</template>
</Upload>
<script setup>
const httpRequest = (files)=> {
// 获取到文件 ,自定已上传方法
}
</script>

总结

通过以上可以实现一个支持多种交互方式的文件上传组件,同时也将 element-plus 中文件上传的流程做了一个学习,因为该组件的实现过程就是参考了 element-plus 的实现,在 element-plus 上传的基础上添加了粘贴上传交互,该组件的实现重在交互方式,各个样式风格通过插槽自定义。


往期推荐



将ChatGPT移植到30年前的操作系统,是怎样的画风?

员工窃取公司游戏源代码,半年盈利1.5亿

助力开发者,基于GPT的开发者实用工具合集



这里有最新开源资讯、软件更新、技术干货等内容

点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦


微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
每日一读 | 让生活改变的几种心态孙燕姿回应成真?AI歌手音乐创作软件上线,人类怎么办?抬气质又百搭,这件上衣太好穿啦!太强啦!!!ChatGPT 能上传文件了,能执行 Python 代码啦!克罗地亚克尔卡国家公园(Krka National Park),大小瀑布第一百一十三章 疑杀ChatGPT 新增六项功能,GPT-4 成默认模型,可上传文件、用快捷键枪和抖音ChatGPT能上传文件了!文档图片数据集秒理解,代码一键执行黑客曝ChatGPT三大新功能:记住你是谁/上传文件/切换工作区,客户端源代码已被扒光瓦格纳叛乱后,俄政局的几种可能走向顶级e人,软件上划遍上海所有美女新买的房车准备自驾罗布泊,结果还没出北京轮子掉了把别人车砸了Cityride火出圈!小小的两个轮子如何撬动消费大市场?一个奇葩的轮子,竟能让9岁孩子搞懂高中物理难点!离苦之前切莫疏忽大意 | 十一、容易疏忽大意的几种情况瓦格纳军事叛乱发生后,俄罗斯政局的几种可能走向如何高效实现文件传输:小文件用零拷贝、大文件用异步io+直接io重磅!华人又要做“小白鼠”?加拿大入境6000人被拒,签证文件上竟有这标记?!一切都源于...如何高效实现文件传输:小文件采用零拷贝、大文件采用异步io+直接io海运的几种放货方式双林奇案录第三部之长命锁: 第十六节不要在半导体领域重复造轮子自来熟猫猫简直是动物界的社交达人,随时随地跨物种交友重磅!加拿大边境6000人被拒,移民文件上竟有这种标记?!一切都源于这种不为人知的AI项目...第一百一十四章 突围李辉披露访乌细节:换3种交通工具,坐18小时绿皮车【WWDC 2023】MR交互篇,如何为眼睛和手设计交互?李辉披露访乌细节:换了3种交通工具,坐了18小时“绿皮火车”数字交互成重要互动媒介, 澳洲急需创新人才! 莫纳什交互设计专业详解孟羽童已从格力离职;4月新开购物中心数量暴增414%;杉杉控制权争夺风波再起;百度造手机;淘宝官方寄件上线|联商头条收到卫生局通知信:不上传疫苗记录,孩子会被停学!大多伦多各区疫苗小黄卡上传攻略王晓东院士:对抗衰老的几种策略ChatGPT增六项功能,GPT-4成默认模型,可上传文件、用快捷键ChatGPT学会自己提问题了,还支持多文件上传
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。