基于Vue3的全屏拖拽上傳組件
本文主要介紹了基于Vue3的全屏拖拽上傳組件,分享給大家,具體如下:
知識(shí)點(diǎn)
- 瀏覽器拖拽 api
- fetch 請(qǐng)求
- vue3
說(shuō)來(lái)話長(zhǎng),長(zhǎng)話短說(shuō),關(guān)于 html5 的拖拽 api 也只是做過(guò)一些拖拽排序的例子.其實(shí)思路上與其他拖拽上傳組件基本一樣,都是指定一個(gè)區(qū)域可拖拽,然后讀取文件在上傳
先說(shuō)說(shuō)拖拽 api,這個(gè)是 html5 新增的一個(gè) api,給一個(gè)元素設(shè)置 draggable = true 屬性時(shí),該元素就會(huì)支持拖拽
拖拽元素事件如下
1. ondrag 當(dāng)拖動(dòng)元素的時(shí)候運(yùn)行腳本
2. ondragstart 當(dāng)拖動(dòng)操作開(kāi)始時(shí)候運(yùn)行腳本
3. ondragend 當(dāng)拖動(dòng)操作結(jié)束的時(shí)候運(yùn)行腳本
目標(biāo)元素的事件如下:
1. ondragover 當(dāng)元素被拖動(dòng)至有效拖放目標(biāo)上方時(shí)執(zhí)行腳本
2. ondragenter 當(dāng)元素被拖動(dòng)至有效拖動(dòng)目標(biāo)時(shí)執(zhí)行腳本
3. ondragleave 當(dāng)元素離開(kāi)至有效拖放目標(biāo)是運(yùn)行腳本
4. ondrop 當(dāng)被拖動(dòng)元素正在被放下的時(shí)候運(yùn)行腳本
比如我們想監(jiān)聽(tīng) body 的拖拽:
const ele = document.querySelector('body')
ele.addEventListener('dragenter', (e) => {
// do something
})
而當(dāng)我們想要阻止默認(rèn)事件的時(shí)候我們可以用 e.preventDefault()
組件
先看一下效果,此時(shí)我這里是設(shè)置的僅能上傳 png 與 jpg

使用:
<upload
accept=".jpg,.png,.ico" // 設(shè)置文件類型
@onChange="change" // 文件上傳事件
action="http://localhost:3001/upload" // 上傳地址
:header="header" // 上傳的header
autoUpload // 是否自動(dòng)上傳
name="file"http:// 上傳的字段名
@onSuccess="onSuccess" // 上傳成功回調(diào)
></upload>
最開(kāi)始的時(shí)候我想獲取拖拽元素的時(shí)候莫名發(fā)現(xiàn)盡管加了監(jiān)聽(tīng)事件,可還是會(huì)打開(kāi)新的窗口去預(yù)覽文件,所以我們第一步就是先把默認(rèn)事件都給禁用掉
// 禁用默認(rèn)拖拽事件
function disableDefaultEvents() {
const doc = document.documentElement
doc.addEventListener('dragleave', (e) => e.preventDefault()) //拖離
doc.addEventListener('drop', (e) => e.preventDefault()) //拖后放
doc.addEventListener('dragenter', (e) => e.preventDefault()) //拖進(jìn)
doc.addEventListener('dragover', (e) => e.preventDefault()) //拖來(lái)拖去
}
直接獲取根元素,阻止拖拽的默認(rèn)事件
第二步就是我們給 body 或是其他元素加上我們想要監(jiān)聽(tīng)的事件,這里有一個(gè)注意的是 body 的高度一定是窗口的高度,這樣才會(huì)全屏拖拽,在拖離的時(shí)候我們還要判斷一下文件是否被拖出區(qū)域
這里一共有這么判斷,
e.target.nodeName === 'HTML',這個(gè)用來(lái)判斷根元素是不是 html
e.target === e.explicitOriginalTarget 這個(gè)是火狐特有的一個(gè) api,判斷這兩個(gè)觸發(fā)事件的目標(biāo)是否一致
(!e.fromElement &&
(e.clientX <= 0 ||
e.clientY <= 0 ||
e.clientX >= window.innerWidth ||
e.clientY >= window.innerHeight))
這個(gè)是用來(lái)判斷鼠標(biāo)當(dāng)前的位置的,是否還在區(qū)域內(nèi)
// 初始化拖入事件
function init() {
// 獲取body元素
const ele = document.querySelector('body')
// 添加事件
//拖后放
ele.addEventListener('dragenter', () => {
show.value = true
})
// 這里判斷鼠標(biāo)拖離
ele.addEventListener('dragleave', (e) => {
if (
e.target.nodeName === 'HTML' ||
e.target === e.explicitOriginalTarget ||
(!e.fromElement &&
(e.clientX <= 0 ||
e.clientY <= 0 ||
e.clientX >= window.innerWidth ||
e.clientY >= window.innerHeight))
) {
show.value = false
}
})
//拖進(jìn)
ele.addEventListener('drop', (e) => {
show.value = false
e.preventDefault()
onDrop(e) // 拖入處理文件的方法
})
}
第三步是處理拖入的文件,此時(shí) accept 是我們定義的文件類型,此時(shí)我們用e.dataTransfer.files這個(gè)屬性可以獲得拖入的文件,
然后我們把拖入的文件用 filter 做一個(gè)過(guò)濾,只保留我們需要的文件類型
checkType(file,accept)就是用來(lái)判斷文件類型的,這一個(gè)函數(shù)是借鑒了 element ui 里面的上傳組件的篩選,當(dāng)時(shí)我也是寫(xiě)蒙了我 😂
// 檢查文件類型
function checkType(file, accept = '') {
const { type, name } = file
if (accept.length === 0) return true
const extension = name.indexOf('.') > -1 ? `.${name.split('.').pop()}` : ''
const baseType = type.replace(/\/.*$/, '')
return accept
.split(',')
.map((type) => type.trim())
.filter((type) => type)
.some((acceptedType) => {
if (/\..+$/.test(acceptedType)) {
return extension === acceptedType
}
if (/\/\*$/.test(acceptedType)) {
return baseType === acceptedType.replace(/\/\*$/, '')
}
if (/^[^/]+\/[^/]+$/.test(acceptedType)) {
return type === acceptedType
}
})
}
這個(gè)方法是文件拖入之后的處理,當(dāng)我們獲得需要的文件之后就是根據(jù)autoUpload來(lái)判斷一下是否上傳
function onDrop(e) {
const accept = props.accept
const list = [].slice.call(e.dataTransfer.files).filter((file) => {
if (accept) {
return checkType(file, accept)
}
return true
})
fileList = list.map((p) => {
return handleStart(p)
})
// 觸發(fā)事件
onChange()
if (props.autoUpload) {
if (props.action === '') {
onError()
throw 'need action'
return
}
list.forEach((file) => {
post(file) // 上傳文件
})
}
}
源碼如下:
<template>
<div class="mask" v-show="show" id="mask">
<h3>拖拽到這里上傳</h3>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
// import ajax from './ajax'
const props = defineProps({
name: String, // 上傳的字段名
header: { Object, Number, String }, // 上傳的文件頭
// 驗(yàn)證的文件類型,有值的時(shí)候只會(huì)拖入所有的文件只會(huì)保留設(shè)置過(guò)濾后的文件
accept: {
type: String,
default: '',
},
// 是否開(kāi)啟自動(dòng)上傳
autoUpload: {
type: Boolean,
default: false,
},
// 上傳地址
action: {
type: String,
default: '#',
},
})
const emit = defineEmits(['onError', 'onProgress', 'onSuccess', 'onChange']) // 默認(rèn)emit事件
let show = ref(false) // 是否展示遮罩
let fileList = reactive([]) // 文件列表
let tempIndex = 0 // 做一個(gè)標(biāo)記
onMounted(() => {
disableDefaultEvents()
init()
})
// 初始化拖入事件
function init() {
const ele = document.querySelector('body')
ele.addEventListener('dragenter', () => {
show.value = true
}) //拖后放
ele.addEventListener('dragleave', (e) => {
if (
e.target.nodeName === 'HTML' ||
e.target === e.explicitOriginalTarget ||
(!e.fromElement &&
(e.clientX <= 0 ||
e.clientY <= 0 ||
e.clientX >= window.innerWidth ||
e.clientY >= window.innerHeight))
) {
show.value = false
}
}) //拖離
ele.addEventListener('drop', (e) => {
show.value = false
e.preventDefault()
onDrop(e)
}) //拖進(jìn)
}
// 禁用默認(rèn)拖拽事件
function disableDefaultEvents() {
const doc = document.documentElement
doc.addEventListener('dragleave', (e) => e.preventDefault()) //拖離
doc.addEventListener('drop', (e) => e.preventDefault()) //拖后放
doc.addEventListener('dragenter', (e) => e.preventDefault()) //拖進(jìn)
doc.addEventListener('dragover', (e) => e.preventDefault()) //拖來(lái)拖去
}
// 拖入時(shí)的事件
function onDrop(e) {
const accept = props.accept
const list = [].slice.call(e.dataTransfer.files).filter((file) => {
if (accept) {
return checkType(file, accept)
}
return true
})
fileList = list.map((p) => {
return handleStart(p)
})
onChange()
if (props.autoUpload) {
if (props.action === '') {
onError()
throw 'need action'
return
}
list.forEach((file) => {
post(file)
})
}
}
// 檢查文件類型
function checkType(file, accept = '') {
const { type, name } = file
if (accept.length === 0) return true
const extension = name.indexOf('.') > -1 ? `.${name.split('.').pop()}` : ''
const baseType = type.replace(/\/.*$/, '')
return accept
.split(',')
.map((type) => type.trim())
.filter((type) => type)
.some((acceptedType) => {
if (/\..+$/.test(acceptedType)) {
return extension === acceptedType
}
if (/\/\*$/.test(acceptedType)) {
return baseType === acceptedType.replace(/\/\*$/, '')
}
if (/^[^/]+\/[^/]+$/.test(acceptedType)) {
return type === acceptedType
}
})
}
// 處理文件列表返回值
function handleStart(rawFile) {
rawFile.uid = Date.now() + tempIndex++
return {
status: 'ready',
name: rawFile.name,
size: rawFile.size,
percentage: 0,
uid: rawFile.uid,
raw: rawFile,
}
}
// 上傳的事件
function post(rawFile) {
const options = {
headers: props.header,
file: rawFile,
data: props.data || '',
filename: props.name || 'file',
action: props.action,
}
upload(options)
.then((res) => {
res.json()
})
.then((json) => {
onSuccess(json, rawFile)
})
.catch((err) => {
onError(err, rawFile)
})
}
// 文件上傳方法
function upload(option) {
const action = option.action
const formData = new FormData()
if (option.data) {
Object.keys(option.data).forEach((key) => {
formData.append(key, option.data[key])
})
}
formData.append(option.filename, option.file, option.file.name)
const headers = new Headers()
for (let item in headers) {
if (headers.hasOwnProperty(item) && headers[item] !== null) {
headers.append(i, option.headers[i])
}
}
return fetch(action, {
mode: 'no-cors',
body: formData,
headers: headers,
method: 'post',
})
}
// 拖拽進(jìn)去獲取文件列表的事件
function onChange() {
emit('onChange', fileList)
}
// 上傳中的事件
function onProgress(e, file) {
emit('onProgress', e, file, fileList)
}
// 上傳成功事件
function onSuccess(res, file) {
emit('onProgress', res, file, fileList)
}
// 上傳失敗事件
function onError() {
emit('onError')
}
</script>
<style scoped>
.mask {
top: 0;
bottom: 0;
right: 0;
left: 0;
position: fixed;
z-index: 9999;
opacity: 0.6;
text-align: center;
background: #000;
}
h3 {
margin: -0.5em 0 0;
position: absolute;
top: 50%;
left: 0;
right: 0;
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
font-size: 40px;
color: #fff;
padding: 0;
}
</style>
到此這篇關(guān)于基于Vue3的全屏拖拽上傳組件的文章就介紹到這了,更多相關(guān)Vue3 全屏拖拽上傳內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue+elementUI組件遞歸實(shí)現(xiàn)可折疊動(dòng)態(tài)渲染多級(jí)側(cè)邊欄導(dǎo)航
這篇文章主要介紹了vue+elementUI組件遞歸實(shí)現(xiàn)可折疊動(dòng)態(tài)渲染多級(jí)側(cè)邊欄導(dǎo)航,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04
淺談vue 組件中的setInterval方法和window的不同
這篇文章主要介紹了淺談vue 組件中的setInterval方法和window的不同,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-07-07
Vue中構(gòu)造數(shù)組數(shù)據(jù)之map和forEach方法實(shí)現(xiàn)
數(shù)組操作是前端最重要的數(shù)據(jù)操作,構(gòu)造數(shù)組數(shù)據(jù),又是數(shù)組操作中很常見(jiàn)的,本文將梳理下map和forEach方法在Vue項(xiàng)目中的使用,感興趣的朋友跟隨小編一起看看吧2022-09-09
vuex學(xué)習(xí)進(jìn)階篇之getters的使用教程
getters用于獲取state里的數(shù)據(jù),它類似于計(jì)算屬性,如果要獲取的數(shù)據(jù)并沒(méi)有發(fā)生變化的話,就會(huì)返回緩存的數(shù)據(jù),下面這篇文章主要給大家介紹了關(guān)于vuex學(xué)習(xí)進(jìn)階篇之getters的使用教程,需要的朋友可以參考下2022-10-10
解決vue-seamless-scroll滾動(dòng)加點(diǎn)贊銜接處數(shù)據(jù)不同步問(wèn)題
這篇文章主要介紹了解決vue-seamless-scroll滾動(dòng)加點(diǎn)贊銜接處數(shù)據(jù)不同步問(wèn)題,初步判斷可能是因?yàn)橄路綉医觱ue-seamless-scroll是靜態(tài)的,沒(méi)同步DOM,本文給大家分享解決方法,感興趣的朋友一起看看吧2021-11-11
vue3 組合式API defineEmits() 與 emits 組
在Vue中,defineEmits()是Vue3組合式API中用于聲明自定義事件的,而emits選項(xiàng)則用于Vue2和Vue3的選項(xiàng)式API中,defineEmits()允許使用字符串?dāng)?shù)組或?qū)ο笮问铰暶魇录?emits選項(xiàng)也支持這兩種形式,且驗(yàn)證函數(shù)可以驗(yàn)證事件參數(shù),這兩種方法都是為了更規(guī)范地在組件間通信2024-09-09
Vue利用高德地圖API實(shí)現(xiàn)實(shí)時(shí)天氣
這篇文章主要為大家詳細(xì)介紹了Vue如何利用高德地圖API實(shí)現(xiàn)實(shí)時(shí)天氣,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-12-12

