先说明几点:1. 使用Vue 2.x。2. 这几个例子是比较适合我自己项目场景的方案,主要为了记录下,仅供参考。样式有引入部分覆盖element-ui的公共样式,因此光用我组件里scoped的样式显示效果不会完全一样。然后单图上传是模拟了element-ui 两种文件列表的样子,实际上属性设置为:show-file-list="false"
,移除功能也是额外定义而非传值实现的。需要留下心。
一、单图上传(父子组件图片地址双向绑定)
我们先看功能和效果。大致分点击按钮上传和拖拽上传,上传后都可以预览(预览弹窗宽度可传参dialogWidth: String
自定义)
1. 点击按钮上传 (不传 drag
参数)
2. 拖拽/点击上传 (传 :drag = "true"
)
注:拖拽无法限制拖入多张图的操作,但我们设置了:multiple="false"
,因此只有一张能上传成功。
3. 组件代码
SingleUpload.vue
<!--单图上传组件/按钮-->
<template>
<div class="upload-container">
<el-upload
:action="uploadUrl"
name="avatar"
:multiple="false"
:show-file-list="false"
:before-upload="beforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:drag="drag"
with-credentials
:class="uploadClass"
>
<div v-if="!drag">
<el-button v-if="imageUrl" size="small" type="success">已上传,可点击修改</el-button>
<el-button size="small" type="primary" v-else><i class="el-icon-upload el-icon--left"></i>点击上传</el-button>
</div>
<div v-if="drag">
<i class="el-icon-upload" />
<div class="el-upload__text">将单张图片拖到<br />此处,或<em>点击上传</em></div>
</div>
</el-upload>
<el-dialog :visible.sync="dialogVisible" :append-to-body="true" :modal-append-to-body="false" :width="dialogWidth" class="preview-dialog">
<img width="100%" :src="imageUrl" alt="" />
</el-dialog>
<div v-if="!drag && imageUrl.length > 0" class="el-upload-list el-upload-list--text">
<div class="el-upload-list__item is-success">
<a class="el-upload-list__item-name" @click="handlePreview"> <i class="el-icon-picture"></i>点此处预览</a>
<label class="el-upload-list__item-status-label">
<i class="el-icon-upload-success el-icon-circle-check"></i>
</label>
<i class="el-icon-close" @click="removeImage"></i>
</div>
</div>
<div v-if="drag && imageUrl.length > 0" class="el-upload-list el-upload-list--picture-card">
<div class="el-upload-list__item is-success">
<img :src="imageUrl" alt="" class="el-upload-list__item-thumbnail" />
<label class="el-upload-list__item-status-label">
<i class="el-icon-upload-success el-icon-check"></i>
</label>
<i class="el-icon-close"></i>
<i class="el-icon-close-tip">按 delete 键可删除</i>
<span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview">
<i class="el-icon-zoom-in" @click="handlePreview"></i>
</span>
<span class="el-upload-list__item-delete">
<i class="el-icon-delete" @click="removeImage"></i>
</span>
</span>
</div>
</div>
<div class="el-upload__tip" slot="tip">只能上传一张图片,格式限jpg/png/gif,大小不超过2M</div>
</div>
</template>
<script>
export default {
name: 'SingleUpload',
props: {
value: {
type: String,
default: ''
},
drag: {
type: Boolean,
default: false
},
dialogWidth: {
type: String,
default: '35%'
}
},
data() {
return {
dialogVisible: false
}
},
computed: {
uploadUrl() {
const url = process.env.VUE_APP_BASE_API + '/admin/single/upload'
return url
},
imageUrl() {
return this.value ? process.env.VUE_APP_BASE_API + this.value : ''
},
uploadClass() {
return this.drag ? 'image-uploader' : ''
}
},
methods: {
removeImage() {
this.$confirm(`确定移除已上传的图片?`, '提示').then(() => {
this.emitInput('')
})
},
emitInput(val) {
this.$emit('input', val)
},
handleUploadSuccess(response) {
this.emitInput(this.commonApi.uploadSuccess(response))
},
handleUploadError(err) {
this.$message.error(err.message)
},
handleUploadRemove() {
this.emitInput('')
},
beforeUpload(file) {
const typeList = ['image/jpeg', 'image/png', 'image/gif']
const isTypeValid = typeList.includes(file.type)
const isLt2M = file.size / 1024 / 1024 < 2
if (!isTypeValid) {
this.$message.error('图片格式只能是 JPG/PNG/GIF!')
}
if (!isLt2M) {
this.$message.error('图片大小不能超过 2MB!')
}
return isTypeValid && isLt2M
},
handlePreview() {
this.dialogVisible = true
}
}
}
</script>
<style lang="scss" scoped>
@import '~@/styles/mixin.scss';
.upload-container {
width: 100%;
position: relative;
display: flex;
flex-direction: row;
@include clearfix;
.image-uploader {
width: 150px;
margin-right: 20px;
.el-icon-upload {
margin: 20px 0 16px;
font-size: 60px;
}
.el-upload__text {
line-height: 20px;
font-size: 13px;
}
}
.el-upload-list--text {
margin: 6px 0 0 10px;
}
.image-uploader /deep/ .el-upload-dragger {
width: 150px;
height: 150px;
}
.el-upload__tip {
margin: 0 0 0 15px;
}
.el-upload-list__item:first-child {
margin: 0;
}
}
.preview-dialog /deep/ .el-dialog__header {
padding: 0;
}
.preview-dialog /deep/ .el-dialog__body {
padding: 20px;
}
</style>
4. 使用
前提是我已经全局注册了upload-single
组件,若单独引用别忘了在当前组件导入和注册。然后一般情况同个项目上传地址都是一致的,就写死在上传组件里了。用起来基本只需关注数据字段,代码非常简洁。
<template>
<div class="app-container">
<!---- * 简单示例,省略其余外层代码 * ---->
<!---- 1. 点击上传 ---->
<el-col :span="24">
<el-form-item label="照片" prop="photoSrc">
<upload-single v-model="newPlant.photoSrc" />
</el-form-item>
</el-col>
<!---- 2. 拖拽上传,同时定义了预览弹窗宽度,和element-ui官方api一致值是百分比 ---->
<el-col :span="24">
<el-form-item label="照片" prop="photoSrc">
<upload-single v-model="newPlant.photoSrc" dialogWidth="30%" drag />
</el-form-item>
</el-col>
</div>
</template>
export default {
name: 'Plant',
data() {
return {
newPlant: {
// ...
photoSrc: ''
}
}
}
一、批量上传
设置 multiple: true
可以让我们同时选取多个文件,然而上传到action
地址的时候,则是选取几个文件便调用几次接口,on-success
也会触发相应的次数。处理上传成功返回数据的时候要特别注意。
除此之外,el-upload也支持手动上传。(这在多图的时候很有必要,对用户来说可以在上传前确认一遍选择的资源是否正确,对服务端来说可以节省不必要的开销)
在组件设置:auto-upload="false"
、ref="upload"
、:http-request="handleUpload"
之后,执行this.$refs.upload.submit()
,便会触发http-request钩子。如果我们在http-request
函数中发起上传请求,则仍是fileList有几个文件就调用几次接口,依旧无法实现在一次请求中上传多个文件。例子如下:
<template>
<el-upload
ref="upload"
name="photoSrc"
:file-list="uploadList"
:auto-upload="false"
:http-request="handleUpload"
multiple
>
<el-button slot="trigger" size="small" type="primary">选取文件</el-button>
<el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload">上传到服务器</el-button>
</el-upload>
</template>
export default {
name: 'MultiUpload',
methods: {
submitUpload() {
this.$refs.upload.submit()
},
handleUpload(params) {
// 如果在这里写上传业务,有多少张图片就会触发多少次上传请求
console.log(params)
},
}
}
如果我们想在选取图片后手动上传,并且提交请求时一次性上传多个文件,就选择放弃http-request
钩子,也不要再调用this.$refs.upload.submit()
。同时on-success
和on-error
也都无用武之地了。
文件状态改变时的钩子on-change
也可以帮助我们方便地获取文件数据。on-change
事件触发的次数也是根据选取图片的张数来的,我们可以在此处理验证文件格式等,代替自动上传时before-upload
进行的逻辑。
action
是el-upload的必选属性,但手动上传的情况这个值其实无意义了,我们可以设置为空字符串或者任意字符 :action="''"
再然后,让用户自己选取图片再确认上传这个功能似乎不太合理(头像上传这种单图的情况相对适用)。一方面直观简洁的操作体验更好,还有可能造成部分场景逻辑不连贯:比如图片上传只是表单的一项而已,最终提交表单的时候,用户选取了文件但没有点击上传,是他/她放弃传还是忘记了呢?所以这种情况我们把上传事件放在提交表单里。
说了这么多,你是不是想说,don't bb,show me the code
具体封装好的
MultiUpload
组件代码如下:
<!--批量上传组件-->
<template>
<div class="upload-container">
<el-upload
ref="upload"
:action="`${apiUrl}/admin/upload`"
name="photoSrc"
with-credentials
:show-file-list="true"
:file-list="fileList"
:on-change="fileListChange"
:on-preview="handlePreview"
:on-remove="handleRemove"
:auto-upload="false"
:drag="drag"
multiple
:class="uploadClass"
>
<el-button v-if="!drag" slot="trigger" size="small" type="primary">选取文件</el-button>
<div v-if="drag">
<i class="el-icon-upload" />
<div class="el-upload__text">将单张图片拖到<br />此处,或<em>点击上传</em></div>
</div>
<!-- <el-button style="margin-left: 10px;" size="small" type="success" @click="handleUpload">上传到服务器</el-button> -->
<div slot="tip" class="el-upload__tip">
<span style="color: #f56c6c;">只能上传jpg/png/gif文件,且不超过2M。</span><br />
图片列表中已经上传成功的图片(<span style="color: #67c23a;">有绿色✓</span>),点击图片名称可以预览大图
</div>
</el-upload>
<el-dialog :visible.sync="dialogVisible" :append-to-body="true" :modal-append-to-body="false" :width="dialogWidth" class="preview-dialog">
<img width="100%" :src="previewUrl" alt="" />
</el-dialog>
</div>
</template>
<script>
import { uploadMult } from '@/api/upload'
export default {
name: 'MultiUpload',
props: {
list: {
type: Array,
default: []
},
drag: {
type: Boolean,
default: false
},
dialogWidth: {
type: String,
default: '35%'
}
},
data() {
return {
value: '',
fileList: [], // 由于不是简单数据,不能直接用props.list值作为初始值
dialogVisible: false
}
},
computed: {
previewUrl() {
return this.value ? process.env.VUE_APP_BASE_API + this.value : ''
},
uploadClass() {
return this.drag ? 'image-uploader' : ''
}
},
watch: {
list: {
handler(newVal) {
// 深拷贝,项目里推荐lodash
// this.fileList = _.cloneDeep(this.list)
this.fileList = this.list.map(obj => ({ ...obj }))
},
deep: true
}
},
methods: {
fileListChange(file, fileList) {
// 添加文件、上传成功和上传失败时都会被调用
const typeList = ['image/jpeg', 'image/png', 'image/gif']
const isTypeValid = typeList.includes(file.raw.type)
const isLt2M = file.size / 1024 / 1024 < 2
if (!isTypeValid) return this.$message.error('图片格式只能是 JPG/PNG/GIF!')
if (!isLt2M) return this.$message.error('图片大小不能超过 2MB!')
this.fileList.push(file) // 能响应式更改数组
// this.$set(this.fileList, this.fileList.length, file) // 能响应式更改数组
},
filterFileList(uploaded) {
// 传 uploaded 筛选已上传的图片列表,不传筛选未上传的
return uploaded ? this.fileList.filter(item => !item.hasOwnProperty('raw')) : this.fileList.filter(item => item.hasOwnProperty('raw'))
},
uploadMultiSuccess(files) {
let photoList = []
files.forEach((pic, i) => {
const src = pic.path.replace('/public', '')
const uid = Date.parse(new Date()) / 1000 + i
photoList.push({ name: pic.name, src, uid, status: 'success' })
})
return photoList
},
async getUploadedList() {
/* 逻辑梳理:
1. 点击父组件表单提交按钮触发,这时说明组件已无其他数据操作
2. 父组件需要的数据是fileList中所有不含raw字段的数据,不含的只有list传过来的新成功上传后经过处理的两种可嫩
3. 首先判断有没有含有raw的,如果没有,直接return fileList, 因为fileList的值就等于传过来的list
4. 如果有去批量上传,请求的返回值追加到this.list中,再返给父组件
*/
const toUploadList = this.filterFileList()
if (toUploadList.length == 0) return this.fileList
let formData = new FormData()
toUploadList.forEach(file => formData.append('photoSrc', file.raw, file.name))
const { data } = await uploadMult(formData)
return this.filterFileList(true).concat(this.uploadMultiSuccess(data))
},
handlePreview(file) {
if (file.src) {
this.value = file.src
this.dialogVisible = true
}
},
handleRemove(file, fileList) {
this.fileList = fileList.map(obj => ({ ...obj }))
}
}
}
</script>
<style lang="scss" scoped>
@import '~@/styles/mixin.scss';
.upload-container {
width: 100%;
position: relative;
display: flex;
flex-direction: row;
flex-flow: wrap;
.image-uploader {
width: 100%;
display: flex;
flex-direction: row;
.el-icon-upload {
margin: 20px 0 16px;
font-size: 60px;
}
.el-upload__text {
line-height: 20px;
font-size: 13px;
}
.el-upload__tip {
width: 300px;
margin: 0 20px;
line-height: 20px;
}
}
.image-uploader /deep/ .el-upload-list__item:first-child {
margin-top: 0px;
}
.image-uploader /deep/ .el-upload-dragger {
width: 150px;
height: 150px;
}
}
</style>
父组件使用
<template>
<div class="app-container">
<!---- * 简单示例,省略其余外层代码 * ---->
<!---- 1. 点击上传 ---->
<el-col :span="24">
<el-form-item label="照片" prop="photoSrc">
<upload-multi ref="upload" :list="newClothing.photoSrc" />
</el-form-item>
</el-col>
<!---- 2. 拖拽上传,同时定义了预览弹窗宽度,和element-ui官方api一致值是百分比 ---->
<el-col :span="24">
<el-form-item label="照片" prop="photoSrc">
<upload-multi v-model="newClothing.photoSrc" dialogWidth="40%" drag />
</el-form-item>
</el-col>
</div>
</template>
export default {
name: 'Clothing',
data() {
return {
newClothing: {
// ...
photoSrc: ''
}
},
method:
}
丑丑的样式就先这样了,懒得再改了,心lui了
在使用sass-loader预处理器的情况下,覆盖element-ui样式的时候遇到了坑,指路:[Vue] scoped覆盖第三方或子组件样式无效
可以看出这两个组件和elementUI大部分是借鉴UI的关系,主要功能大多自行实现,因此稍微修改下就可以应用到大部分库(如vuetify、iview)。
以上均为个人拙见,功能/业务逻辑大家觉得有不合理之处或者更好的处理方法,欢迎留言指点。