前言
最近开发需求的时候,不同的页面,ui设计师提供了不同的文件上传样式,在现有的上传组件中修改难免会造成耦合,便想着重构一下。
实现思路
介于上传的样式多种多样,所以考虑将文件选择和上传,同ui样式做分离,这样基础组件只需要负责文件的选取和上传,而ui样式则交给其它的组件来负责。
代码结构
c-upload 负责文件的选取和公共上传逻辑
FileInfo类表明文件实例
utils提供一些公共方法
picture-upload为ui组件之一,负责样式的渲染。
具体实现
c-upload
文件上传基础组件,只负责文件的选取和上传。
通过传入slot来自定义文件上传样式。
action属性为传入的上传方法,如果非选中直接上传,可以忽略该方法,在ui组件内部进行手动上传。
beforeUpload负责在上传前进行拦截
<template>
<div class="c-upload" @click="triggerFileInput">
<slot></slot>
<input
ref="fileInput"
class="file-input"
@change="handleFileChange"
:multiple="multiple"
:accept="accept"
type="file" />
</div>
</template>
<script>
import FileInfo, { EnumFileStatus } from './FileInfo'
export {
FileInfo,
EnumFileStatus
}
/**
* 错误类型
*/
export const EErrorType = {
ActionNotDefined: 1,
BeforeUploadFalse: 1 << 1,
uploadError: 1 << 2
}
export default {
props: {
accept: {
type: String,
default: '*'
},
// 是否多选
multiple: {
type: Boolean,
default: false
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 上传方法
action: {
type: Function
},
// 上传前
beforeUpload: {
type: Function
}
},
methods: {
// 触发文件上传
triggerFileInput() {
// 禁用 中止
if (this.disabled) {
return
}
this.$refs.fileInput.click()
},
// 监听文件改变
handleFileChange(e) {
const { action } = this
const newFiles = [...e.target.files]
const newFileInfos = []
newFiles.forEach(file => newFileInfos.push(new FileInfo(file)))
this.$emit('change', newFileInfos)
this.clearFileInputValue()
action && this.executeFileUpload(newFileInfos)
},
// 执行文件上传
executeFileUpload(fileInfos) {
// 批量文件上传
fileInfos.forEach(fileInfo => this.uploadFile(fileInfo))
},
// 上传单个文件
async uploadFile(fileInfo) {
const { action, beforeUpload } = this
if (!action) {
this.emitError(fileInfo, { code: EErrorType.ActionNotDefined, msg: 'action is not defined' })
return
}
// 上传前钩子
if (beforeUpload) {
const res = beforeUpload(fileInfo)
// 返回false 中止上传
if (res === false) {
this.emitError(fileInfo, { code: EErrorType.BeforeUploadFalse, msg: 'beforeUpload返回false,中止上传' })
return
}
}
try {
// 上传中
fileInfo.setStatus(EnumFileStatus.Uploading)
await action(fileInfo)
fileInfo.setStatus(EnumFileStatus.UploadSucceed)
this.$emit('success', fileInfo)
} catch (err) {
this.emitError(fileInfo, { code: EErrorType.uploadError, msg: err })
}
},
// 通知失败
emitError(fileInfo, err) {
// 失败
fileInfo.setStatus(EnumFileStatus.UploadFailed)
console.error(err)
this.$emit('error', fileInfo, err)
},
// 清空选中的file文件
clearFileInputValue() {
this.$refs.fileInput.value = ''
}
}
}
</script>
<style lang="scss" scoped>
.c-upload {
display: inline-block;
.file-input {
display: none;
width: 0;
height: 0;
font-size: 0;
}
}
</style>
FileInfo
定义了文件实例,status有四种状态
import { isFile, isString } from '../utils'
/**
* 文件状态
*/
export const EnumFileStatus = {
// 未上传
NotUploaded: 1,
// 上传中
Uploading: 2,
// 上传成功
UploadSucceed: 3,
// 上传失败
UploadFailed: 4
}
/**
* 文件实例
*/
export default class FileInfo {
name = ''
url = ''
_status = EnumFileStatus.NotUploaded
_progress = 0
// Blob
_file = null
constructor(data) {
if (isFile(data)) {
this.name = data.name
this._file = data
} else if (isString(data)) {
this.url = data
this.setProgress(100)
this._status = EnumFileStatus.UploadSucceed
}
}
get progress() {
return this._progress
}
get file() {
return this._file
}
get status() {
return this._status
}
get success() {
return this.status === EnumFileStatus.UploadSucceed
}
// 设置上传状态
setStatus(status) {
this._status = status
}
// 设置url
setUrl(url) {
this.url = url
}
// 设置进度
setProgress(progress) {
this._progress = progress
}
}
utils
公共方法,uploadFile为默认的文件上传方法,可通过传入action属性进行自定义。
/**
* 获取类型
* @param {*} value
* @returns null | string
*/
const getType = value => {
const matched = Object.prototype.toString.call(value).match(/^\[object\s(.*?)\]$/)
return matched ? matched[1] : null
}
export const isFile = value => getType(value) === 'File'
export const isString = value => getType(value) === 'String'
// 文件上传公用方法
export async function uploadFile(fileInfo) {
try {
const formData = new FormData()
formData.append('iconFile', fileInfo.file)
let curUpLoadProgress = 0
fileInfo.setProgress(0)
const res = await this.$http({
url: '上传的url',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
},
// 上传进度
onUploadProgress(progressEvent) {
if (progressEvent.lengthComputable) {
curUpLoadProgress = Math.floor(progressEvent.loaded / progressEvent.total * 100)
fileInfo.setProgress(curUpLoadProgress)
}
}
})
// 上传成功
if (res.status === '0' && res.data) {
fileInfo.setUrl(res.data)
} else {
// 上传失败
return Promise.reject(new Error('数据格式异常'))
}
} catch (error) {
return Promise.reject(error)
}
}
picture-upload
负责ui样式的展示,在接收到选取的文件后进行展示。
<template>
<div class="pic-upload">
<div class="inner-container">
<div class="list-item" v-for="(item, index) in fileList" :key="index">
<el-progress
v-if="item.status === EnumFileStatus.Uploading"
:width="70"
type="circle"
:percentage="item.progress"></el-progress>
<div class="img-container" v-else-if="item.url">
<img :src="item.url" alt="图片" />
<i @click.stop="handleDelete(index)" class="img-delete el-icon-error"></i>
</div>
</div>
<!-- 超出限制 隐藏上传按钮 -->
<CUpload
v-if="!uploadDisabled"
accept="image/*"
:action="uploadFile"
:before-upload="beforeUpload"
@change="handleUploadChange"
@error="handleUploadError">
<div class="upload-container">
<div class="upload-btn">
<i class="el-icon-plus"></i>
<p>上传</p>
</div>
</div>
</CUpload>
</div>
</div>
</template>
<script>
import CUpload, { EnumFileStatus, EErrorType } from '../c-upload'
import { uploadFile } from '../utils'
import emitter from 'element-ui/src/mixins/emitter'
export {
EnumFileStatus
}
export default {
mixins: [emitter],
components: {
CUpload
},
props: {
value: {
type: Array,
default: () => []
},
limit: {
type: Number
},
beforeUpload: {
type: Function
}
},
data() {
return {
EnumFileStatus
}
},
computed: {
fileList: {
get() {
return this.value || []
},
set(value) {
this.$emit('input', value)
}
},
// 是否禁止上传
uploadDisabled() {
const { limit, fileList } = this
if (limit && fileList.length >= limit) {
return true
}
return false
}
},
methods: {
// 选择文件
handleUploadChange(fileInfos) {
// 保存新选中的实例
this.fileList.push(...fileInfos)
this.dispatch('ElFormItem', 'el.form.change', this.fileList)
},
// 删除文件
handleDelete(index) {
this.deleteFileByIndex(index)
},
// 上传失败
handleUploadError(fileInfo, err) {
// 删除保存的文件实例
const index = this.findIndexByFile(fileInfo)
if (~index) {
this.deleteFileByIndex(index)
}
if (err.code & EErrorType.uploadError) {
this.$message.error('文件上传失败')
}
},
// 根据下标 删除文件
deleteFileByIndex(index) {
this.fileList.splice(index, 1)
this.dispatch('ElFormItem', 'el.form.change', this.fileList)
},
// 根据文件 寻找下标
findIndexByFile(fileInfo) {
return this.fileList.findIndex(item => item === fileInfo)
},
// action
uploadFile
}
}
</script>
<style lang="scss" scoped>
.pic-upload{
.inner-container {
display: flex;
flex-wrap: wrap;
padding: 8px;
margin-right: -20px;
margin-bottom: -20px;
overflow: hidden;
.list-item,
.upload-container {
width: 80px;
height: 80px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0 20px 20px 0;
border-radius: 10px;
border: 1px dashed #ccc;
}
.list-item {
.img-container {
position: relative;
width: 100%;
height: 100%;
padding: 10px;
> img {
display: block;
width: 100%;
height: 100%;
object-fit: scale-down;
}
.img-delete {
position: absolute;
top: -8px;
right: -8px;
color: #bbb;
font-size: 20px;
cursor: pointer;
}
}
}
.upload-container {
cursor: pointer;
.upload-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 10px;
> i {
font-size: 26px;
line-height: 26px;
color: #ccc;
margin-bottom: 5px;
}
> p {
font-size: 13px;
line-height: 13px;
color: #aaa;
}
}
}
}
}
</style>
</style>
基本使用
<template>
<PictureUpload
v-model="fileList"
accept="image/png,image/jpg"
:before-upload="beforeUpload"
:limit="1"></PictureUpload>
</template>
<script>
import {} from '@/compon'
export default {
data() {
return {
fileList: []
}
},
methods: {
// 上传前拦截钩子
beforeUpload(fileInfo) {
if (/\.(png|jpg)$/.test(fileInfo.name)) {
return true
}
this.$message.warning('仅支持.png,.jpg格式')
return false
}
}
}
</script>
上传前 | 上传后 |
---|---|