文章涉及的其他组件可从专栏查询
功能简介
- 可传入Limit对象对文件上传做限制:文件大小、文件数量
- 可通过accept限制文件类型
- 可通过beforeUpload自定义上传前的验证,返回布尔值
- 支持多文件上传
- 可自定义上传控件
- 可以显示上传成功的文件列表
使用
<s-upload @onSuccess="onSucessUpload" :showFiles="true" action=" ">
<s-button :disabled="version.isUpdate">浏览</s-button>
</s-upload>
效果
Types
// 文件上传限制,可自由扩展
type Limit={
size?:number, // 文件大小 单位M
maxFiles?:number, // 文件数量
[index:string]:string|number|undefined
}
// 文件状态
enum FILE_STATUS{
EMPTY=0, // 空
SUCCESS=1, // 上传成功
ERROR=2, // 上传失败
UPLOADING=3 // 上传中
}
// 组件状态
type State={
fileData:any[]|object,// 当前文件
fileStatus:FILE_STATUS, // 文件上传状态
fileList:FileList|[], // 文件列表
fileIndex:number // 文件列表的处理索引
}
// Upload属性
type UploadProp={
action?: string, // 上传链接
initFile?: Array<any> | Object,// 初始文件
accept?: string | Array<string>,// 允许上传的格式
limit?: Limit, // 上传限制
multiple?:boolean,// 是否允许多选,
beforeUpload: (files:FileList)=>boolean,// 上传前处理函数
showFiles: boolean, // 是否显示文件信息
help: string// 辅助信息
}
封装
<template>
<div class="upload-container">
<!-- 上传控件 -->
<div class="trigger-container" @click="onUpload">
<input
class="hidden"
ref="fileUploader"
type="file"
:multiple="multiple"
:accept="acceptType"
@change="fileChange"
/>
<slot></slot>
</div>
<!-- 提示信息 -->
<div v-if="help" class="file-help">
{{help}}
</div>
<!-- 文件信息 -->
<ul class="files-container" v-if="showFiles">
<li v-for="file in fileList" :key="file.name" class="sspace-vertical">
<s-icon icon="icon-file" type="symbol"/>
<span class="sspace-horizon">{{file.name}}</span>
</li>
</ul>
</div>
</template>
<script lang='ts'>
import $axios from "@/request"
import { PropType, computed, ref, watch, reactive, toRefs } from "vue"
import { Message, SIcon } from "@/components"
import { Limit, FILE_STATUS, State} from "@types"
export default {
name: "s-upload",
components: { SIcon },
props: {
// 上传连接
action: String,
// 初始文件
initFile: {
type: [Array, Object],
default: null
},
// 允许上传的格式,数组会被转为字符串,支持的字符串参照mdn文档
accept: {
type: [String, Array],
default: "image/*"
},
// 上传限制
limit: Object as PropType<Limit>,
// 是否允许多选
multiple: {
type: Boolean,
default: false
},
// 上传前处理函数
beforeUpload: Function as PropType<(files:FileList)=>boolean>,
// 是否显示文件信息
showFiles: {
type: Boolean,
default: false
},
// 辅助信息
help: String
},
emits: ["onSuccess", "onError"],
setup (props, context) {
// 原生的input dom
const fileUploader = ref<null | HTMLInputElement>(null)
// 校验accept格式
const acceptType = computed(() => {
if (typeof props.accept !== "string") {
if (Array.isArray(props.accept)) {
return props.accept.join()
} else {
console.error("accept接收字符串或数组,请输入正确的格式")
}
}
return props.accept
})
// 组件状态
const state = reactive<State>({
fileData: props.initFile, // 初始文件
fileStatus: FILE_STATUS.ERROR, // 文件上传状态
fileList: [], // 当前文件列表
fileIndex: 0 // 文件处理索引(处理进度索引)
})
// 监听是否有初始文件,有则重新赋值
watch(() => props.initFile, (val) => {
if (val) {
state.fileStatus = FILE_STATUS.SUCCESS
state.fileData = val
}
})
// 调用原生的upload
const onUpload = (e:Event) => {
if (fileUploader.value) {
fileUploader.value.click()
}
}
// 自定义验证 处理beforeUploade。
const customCheck = async (files:FileList) => {
return new Promise((resolve, reject) => {
// 判断有无改属性
if (props.beforeUpload) {
// 自定义验证的结果
const result = props.beforeUpload(files)
if (typeof result !== "boolean") {
reject(new Error("beforeUploadu应该返回一个布尔值"))
}
resolve(result)
} else {
resolve(true)
}
})
}
// 文件大小验证(组件自带的 通过limit.size定义的)
const sizeCheck = (files:FileList) => {
return new Promise((resolve, reject) => {
const { size } = props.limit
if (size) {
let index = 0
while (index < files.length) {
const file = files[index]
const fileSize = file.size / 1024
if (fileSize > size) {
const msg = `${file.name}文件大小超出${size}K,请重新调整!`
Message.error(msg)
reject(new Error(msg))
}
index++
}
resolve(true)
}
resolve(true)
})
}
// 文件数量验证(组件自带的 通过limit.maxFiles定义的)
const lengthCheck = (files:FileList) => {
return new Promise((resolve, reject) => {
const { maxFiles } = props.limit
if (maxFiles) {
console.log(files.length, maxFiles)
if (files.length > maxFiles) {
const msg = `文件数量不得超过${maxFiles}个`
Message.error(msg)
reject(new Error(msg))
}
resolve(true)
}
resolve(true)
})
}
// 上传前处理
const fileChange = async (e:Event) => {
const target = e.target as HTMLInputElement
const files = target.files
if (files && file.length) {
// 上传前验证
await customCheck(files)
if (props.limit) {
await sizeCheck(files)
await lengthCheck(files)
}
// 本地 不上传到服务器时,验证完直接传回
if (!props.action) {
context.emit("onSuccess", files)
state.fileList = files
state.fileStatus = FILE_STATUS.SUCCESS
} else {
// 否则依次开始上传
state.fileStatus = FILE_STATUS.UPLOADING
state.fileList = files
state.fileIndex = 0
uploadFile(state.fileList[state.fileIndex])
}
}
}
// 上传文件
const uploadFile = async (file:File) => {
try {
const fd = new FormData()
fd.append("file", file)
const data = await $axios.upload(props.action, fd)
// 判断接口返回的数据决定是否继续上传,成功时:
if (data) {
await isFinish()
} else {
throw new Error(`${file.name}在上传过程中发生错误,上传中止`)
}
} catch (err) {
state.fileStatus = FILE_STATUS.ERROR
state.fileList = []
context.emit("onError",`${file.name}在上传过程中发生错误,上传中止`)
} finally {
// 重置索引
state.fileIndex = 0
}
state.fileStatus = FILE_STATUS.SUCCESS
context.emit("onSuccess", state.fileList)
state.fileList = []
}
// 遍历所有文件
const isFinish = () => {
return new Promise((resolve, reject) => {
// 如果有多个文件
if (props.multiple && state.fileList.length > 1) {
// 判断当前文件索引和文件列表长度
if (state.fileIndex < state.fileList.length - 1) {
state.fileIndex++
uploadFile(state.fileList[state.fileIndex])
} else {
resolve(FILE_STATUS.SUCCESS)
}
} else {
resolve(FILE_STATUS.SUCCESS)
}
})
}
return {
acceptType,
fileChange,
onUpload,
fileUploader,
...toRefs(state),
...toRefs(props)
}
}
}
</script>
<style scoped lang="less">
.upload-container{
display: flex;
flex-direction: column;
.file-help{
color:@help;
margin: 10px 0;
font-size: 0.85em;
}
.files-container{
cursor: default;
}
}
</style>