前几天公司做一个临时招聘页,为了方便直接使用了Vue CDN 链接,组件也直接写进了 JS 文件,也没有做模块化分割。不过页面需要上传个人简历照片等信息,于是便手写了一个多文件上传组件,下面附上源码和一些注释
主要功能
- 支持拖拽上传
- 支持多文件选择
- 支持复制粘贴上传(由于浏览器限制,粘贴功能的有使用范围,可以用其他软件截图,比如QQ,然后在上传域粘贴即可)
- 支持上传预览
- 页面效果图:
- mutipleUpload.vue
<template>
<div :class="cls.container">
<!-- 这儿绑定了拖拽事件和粘贴事件 -->
<div :class="cls.area"
@drop.prevent="onDrop"
@dragover.prevent="dragOver = true"
@dragleave.prevent="dragOver = false"
@paste.prevent="onPaste">
<!-- 这儿是中间的提示信息,可以传入一个子标题 subtitle,如:最多可上传 XX 个 jpg 文件 -->
<div v-if="!images.length" :class="cls.tips">
<div>点击 “+” 号或拖拽文件到此处</div>
<div :class="cls.subtitle">{{subtitle}}</div>
</div>
<!-- 这儿是选中文件的预览列表 -->
<div v-for="(item, index) of images" :title="item.name" :key="index"
:class="cls.item" :data-title="item.name">
<img :src="item.src">
<span :class="cls.close">×</span>
</div>
<!-- 这儿是点击选择上传文件按钮 -->
<label :for="nextId" :class="cls.img">
<span :class="cls.choice">+</span>
</label>
</div>
<!-- 这儿是上传域脚提示信息部分 -->
<div :class="cls.foot">
<span>已选择 {{images.length}} 张图片,共 {{getSize}}</span>
<div class="float-right">
<!-- 整个就这一个普通按钮,就不贴按钮的样式了,谁的页面没有按钮的基本样式不是 -->
<button class="im-btn" @click="getData">开始上传</button>
</div>
<input :id="nextId" @change.prevent="onChoice" multiple
style="display: none !important;" type="file">
</div>
</div>
</template>
<script>
// 为什么这么写?母鸡啊,这不是重点
var namespace = 'im'
function toClass (name) {
if (namespace) {
if (namespace[-1] === '-') {
return namespace + name
} else {
return namespace + '-' + name
}
} else {
return name
}
}
toClass.useSubNS = function (ns) {
var prefix = toClass(ns).trim()
prefix = prefix ? (prefix[-1] === '-' ? prefix : prefix + '-') : prefix
return function (name) {
return name ? prefix + name : prefix.slice(0, -1)
}
}
const creator = toClass.useSubNS('upload')
const classes = {
container: creator('container'),
area: creator('area'),
item: creator('item'),
img: creator('add'),
choice: creator('sign'),
foot: creator('foot'),
close: creator('close'),
tips: creator('tips'),
subtitle: creator('subtitle')
}
const megaByte = 1024 * 1024
export default {
props: {
// 上传地址,就像 form 标签的 action
action: String,
count: {
type: Number,
default: 99
},
subtitle: String
},
computed: {
// 每个组件都有自己的唯一 ID
nextId: function () {
return (Math.random() + '').substring(2)
},
// 组件内 class 列表
cls: function () {
return classes
},
// 计算当前选中的文件总大小
// 小于 1MB 按 KB 显示
// 小于 1GM 按 MB 显示(不过不建议上传这么大的文件,目前也不支持大文件上传)
getSize: function () {
let size = this.size / 1024
if (size < 1024) {
return size.toFixed(2) + ' KB'
} else if ((size = size / 1024) < 1024) {
return size.toFixed(2) + ' MB'
} else {
return (size / 1024).toFixed(2) + ' GB'
}
}
},
data () {
return {
size: 0,
images: [],
dragOver: false, //
visible: false
}
},
methods: {
// 很抱歉,这儿把上传的 ajax 部分给阉割了,但构建好了 FormData,直接调就可以
getData: function () {
let data = new FormData()
for (let item of this.images) {
data.append(item.name, item.file)
}
console.log(data)
console.log(megaByte)
},
// 处理拖拽图片事件
onDrop: function (event) {
let data = event.dataTransfer
if (data) {
this.readFiles(data.files)
}
},
// 处理粘贴图片事件
onPaste: function (event) {
let data = event.clipboardData
if (data) {
this.readItems(data.items)
}
},
// 处理选择图片事件
onChoice: function (event) {
this.readFiles(event.target.files)
},
readItems: function (items, file) {
for (let item of items) {
if (item.kind === 'file' && (file = item.getAsFile())) {
this.readImage(file.getAsFile ? file.getAsFile() : file)
}
}
},
readFiles: function (files) {
for (let file of files) {
this.readImage(file.getAsFile ? file.getAsFile() : file)
}
},
// 主要通过 FileReader 实现图片预览功能,也没有采用 iframe 来兼容低版本浏览器
// 不得不说 H5 真的为很多操作都提供了很大的便利
readImage: function (file) {
if (file && file.size && file.type) {
let that = this
let reader = new FileReader()
that.size += file.size
reader.onload = function () {
that.images.push({
checked: false,
src: reader.result,
file: file,
name: file.name,
uploaded: false
})
}
reader.readAsDataURL(file)
}
}
}
}
</script>
下面是样式
/* 容器 */
.im-upload-container {
border: 1px solid #d2d2d2;
border-radius: 0.3em;
}
.im-upload-container .im-upload-foot {
display: block;
height: 46px;
line-height: 46px;
padding: 0 15px;
border-top: 1px solid #d2d2d2;
}
/* 中间的提示 */
.im-upload-tips {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
user-select: none;
}
.im-upload-area {
position: relative;
display: block;
padding: 15px 0 0 15px;
}
.im-upload-area:hover {
box-shadow: 0 0 0 1px #d2d2d2 inset;
}
.im-upload-area > .im-upload-tips {
color: #ddd;
font-size: 22px;
cursor: default;
text-align: center;
}
.im-upload-area > .im-upload-tips > .im-upload-subtitle {
font-size: 18px;
}
.im-upload-area > .im-upload-item {
position: relative;
display: inline-block;
width: 100px;
height: 100px;
margin-bottom: 15px;
margin-right: 15px;
overflow: hidden;
box-shadow: 0 0 0 0 #d2d2d2;
transition-duration: 0.2s;
border-radius: 0.3em;
}
.im-upload-area > .im-upload-item.im-uploaded {
box-shadow: 0 0 0 3px #008B45;
}
.im-upload-area > .im-upload-item.im-uploaded:hover {
box-shadow: 0 0 5px 5px #006231;
}
.im-upload-area > .im-upload-item::before {
content: attr(data-title);
display: block;
width: 100%;
height: 0;
position: absolute;
background: rgba(35, 35, 35, 0.6);
line-height: 25px;
text-align: center;
color: #fefefe;
overflow: hidden;
transition-duration: 0.2s;
}
.im-upload-area > .im-upload-item:hover {
box-shadow: 0 0 5px 5px #bebebe;
}
.im-upload-area > .im-upload-item:hover::before {
height: 25px;
}
.im-upload-area > .im-upload-item > .im-upload-close {
position: absolute;
display: block;
top: 3px;
right: 3px;
z-index: 1;
background: #000;
color: #ffffff;
width: 18px;
height: 18px;
text-align: center;
border-radius: 50%;
}
.im-upload-area > .im-upload-item > img {
vertical-align: middle;
height: 100%;
}
.im-upload-add {
margin-bottom: 15px;
position: relative;
display: inline-block;
width: 100px;
height: 100px;
border: 3px dashed #d2d2d2;
transition-duration: 0.2s;
vertical-align: top;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
border-radius: 0.3em;
cursor: pointer;
}
.im-upload-add > .im-upload-sign {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 140px;
color: #d2d2d2;
transition-duration: 0.2s;
}
.im-upload-add > .im-upload-sign:hover {
color: #bebebe;
}
.im-upload-add:hover {
border-color: #bebebe;
}
.float-right {
float: right;
}
看了很多上传组件,包括一些组件库,实现都很完善,也很强大,这儿主要介绍一下这个组件的一些关键点:
- 粘贴上传:暂时没发现哪些组件实现的这个功能,实现方式主要是对整个上传域监听一个 paste 事件,其实这个事件还可以做更多事,不过这需要结合一定的场景,可以天马行空发挥一下想象力;
- 拖拽上传:和 mousedown、mousemove、mouseup 写拖拽控件相似,注意一下频繁触发事件即可,这儿用了一个变量 dragOver 来屏蔽多余的触发;
- 选择文件按钮:控件在列表末尾放了一个大大的“+”号,这也是很多地方比较通用的方式,这个“+”号实际是一个 label 标签,利用的 label 和 input 标签的传递特性(通过 input 的 id 绑定,这也是为什么每个组件都有自己的唯一 id 的原因),此时就可以完全隐藏 input 标签,然后对 label 标签任意发挥样式的书写。
- 其他:图片预览用到了 FileReader ,这是 h5 的特性,它的 readAsDataURL 方法可以直接读取图片文件为一个 base64 编码的 url,可以直接用 img 标签显示。
也可以放进 background 里面,但是用背景渲染出来的样式很卡,也做不了更多事,不建议这么用。
- 如果出现“+”号位置对不齐的情况加上(是因为字体的原因):
html {
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei","微软雅黑", Arial, sans-serif;
}
- 注意:没有针对不同文件类型的处理