上传 Upload
赞助
文件选择上传和拖拽上传控件
何时使用
- 当需要上传一个或一些文件时
- 当需要展现上传的进度时
- 当需要使用拖拽交互时
基本使用
Upload
Show Code
vue
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
import type { UploadFileType } from 'vue-amazing-ui'
const files = ref<UploadFileType[]>([])
watchEffect(() => {
console.log('files', files.value)
})
function onDrop(e: DragEvent) {
console.log('drop', e)
}
function onChange(files: UploadFileType[]) {
console.log('change', files)
}
function onPreview(file: UploadFileType) {
console.log('preview', file)
}
</script>
<template>
<Upload v-model:fileList="files" @drop="onDrop" @change="onChange" @preview="onPreview" />
</template>非受控模式
不传 v-model:fileList,用 defaultFileList 预填文件列表;继续上传、删除照常生效
Show Code
vue
<template>
<Upload
:default-file-list="[
{
name: '1.jpg',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/1.jpg'
}
]"
/>
</template>禁用
只能预览,不能删除和上传
Show Code
vue
<script setup lang="ts">
import { ref } from 'vue'
import type { UploadFileType } from 'vue-amazing-ui'
const fileList = ref<UploadFileType[]>([
{
name: '1.jpg',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/1.jpg'
},
{
name: 'Markdown.pdf',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/Markdown.pdf'
}
])
</script>
<template>
<Upload disabled v-model:fileList="fileList" />
</template>限制数量
通过 maxCount 限制上传数量;当为 1 时占满后仍显示上传入口,再次上传即替换当前文件(示例已预置 1 个文件)
Show Code
vue
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
import type { UploadFileType } from 'vue-amazing-ui'
const singleFile = ref<UploadFileType[]>([
{
name: '1.jpg',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/1.jpg'
}
])
const fileList = ref<UploadFileType[]>([
{
name: '1.jpg',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/1.jpg'
},
{
name: 'Markdown.pdf',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/Markdown.pdf'
}
])
watchEffect(() => {
console.log('singleFile', singleFile.value)
})
</script>
<template>
<Space vertical>
<Upload :max-count="1" tip="maxCount: 1" v-model:fileList="singleFile" />
<Upload :max-count="3" tip="maxCount: 3" v-model:fileList="fileList" />
</Space>
</template>多文件上传
可一次选择多个文件进行上传
Upload
Show Code
vue
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
import type { UploadFileType } from 'vue-amazing-ui'
const files = ref<UploadFileType[]>([])
watchEffect(() => {
console.log('files', files.value)
})
</script>
<template>
<Upload multiple v-model:fileList="files" />
</template>拖拽上传
draggable 默认开启,把文件拖入上传框即可上传;拖拽路径与点选共用 multiple / accept 约束,类型不符的文件经 reject 回传
拖拽图片到此
Show Code
vue
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
import { useMessage } from 'vue-amazing-ui'
import type { UploadFileType } from 'vue-amazing-ui'
const message = useMessage()
const dragList = ref<UploadFileType[]>([])
watchEffect(() => {
console.log('dragList', dragList.value)
})
function onDrop(e: DragEvent) {
console.log('drop', e)
}
// 拖拽投放的文件不符合 accept 时触发:点选不会走到这里(<input accept> 已由浏览器拦截)
function onReject(rejectedFiles: File[]) {
console.log('reject', rejectedFiles)
message.warning(`已忽略 ${rejectedFiles.length} 个类型不符的文件`)
}
</script>
<template>
<Upload
accept="image/*"
tip="拖拽图片到此"
v-model:fileList="dragList"
@drop="onDrop"
@reject="onReject"
/>
</template>自定义样式布局
缩略图等比覆盖;上传描述文字使用:上传
Show Code
vue
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
import type { UploadFileType } from 'vue-amazing-ui'
const fileList = ref<UploadFileType[]>([
{
name: '1.jpg',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/1.jpg'
},
{
name: 'Markdown.pdf',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/Markdown.pdf'
}
])
</script>
<template>
<Upload tip="上传" fit="cover" v-model:fileList="fileList" />
</template>排列间距使用:'middle';自定义上传 uploading 动画样式
Show Code
vue
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
import type { UploadFileType } from 'vue-amazing-ui'
const fileList = ref<UploadFileType[]>([
{
name: '1.jpg',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/1.jpg'
},
{
name: 'Markdown.pdf',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/Markdown.pdf'
}
])
</script>
<template>
<Upload
:space-props="{ gap: 'middle' }"
:spin-props="{ indicator: 'dot-wheel', color: '#ff6900', tip: '上传中...' }"
v-model:fileList="fileList"
/>
</template>操作完成的消息提示
全局提示由使用方监听 @success / @remove / @error 后调用
Show Code
vue
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
import { useMessage } from 'vue-amazing-ui'
import type { UploadFileType } from 'vue-amazing-ui'
const message = useMessage()
const fileList = ref<UploadFileType[]>([
{
name: '1.jpg',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/1.jpg'
},
{
name: 'Markdown.pdf',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/Markdown.pdf'
}
])
function onSuccess(file: UploadFileType) {
console.log('upload success', file)
message.success('upload success')
}
function onRemove(file: UploadFileType) {
console.log('upload remove', file)
message.success('remove success')
}
function onError(err: unknown) {
console.log('upload error', err)
message.error(err instanceof Error ? err.message : String(err))
}
// 上传失败演示:custom 模式 reject 会触发 @error,由使用方自行提示
function onUploadFailRequest(file: File) {
console.log('upload fail request', file.name)
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('模拟上传失败,请重试'))
}, 1000)
})
}
</script>
<template>
<Space vertical>
<Upload
v-model:fileList="fileList"
@success="onSuccess"
@remove="onRemove"
/>
<Upload
upload-mode="custom"
:custom-request="onUploadFailRequest"
v-model:fileList="fileList"
@error="onError"
/>
</Space>
</template>上传文件校验
上传文件最大 500KB;同时限定文件类型(拖拽路径的类型过滤与 reject 回传见「拖拽上传」一节)
Show Code
vue
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
import { useMessage } from 'vue-amazing-ui'
import type { UploadFileType } from 'vue-amazing-ui'
const message = useMessage()
const imageList = ref<UploadFileType[]>([
{
name: '1.jpg',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/1.jpg'
}
])
const pdfList = ref<UploadFileType[]>([
{
name: 'Markdown.pdf',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/Markdown.pdf'
}
])
watchEffect(() => {
console.log('imageList', imageList.value)
})
watchEffect(() => {
console.log('pdfList', pdfList.value)
})
function onBeforeImageUpload(file: File) {
if (file.size > 500 * 1024) {
// 文件大于 500KB 时取消上传
message.warning('文件必须小于 500KB')
return false // 停止上传
}
console.log('file', file)
console.log('type', file.type)
if (!file.type.includes('image')) {
// 继续上传
message.error('只能上传图片')
return false // 停止上传
}
return true // 继续上传
}
function onBeforePdfUpload(file: File) {
const acceptTypes = ['application/pdf']
if (file.size > 500 * 1024) {
// 文件大于 500KB 时取消上传
message.warning('文件必须小于 500KB')
return false // 停止上传
}
if (!acceptTypes.includes(file.type)) {
// 继续上传
message.error('只能上传 pdf 格式的文件')
return false // 停止上传
}
return true // 继续上传
}
function onChange(files: UploadFileType[]) {
console.log('change', files)
}
function onRemove(file: UploadFileType) {
console.log('remove', file)
}
</script>
<template>
<Space vertical>
<Upload
accept="image/*"
tip="Only Image"
:before-upload="onBeforeImageUpload"
v-model:fileList="imageList"
@change="onChange"
@remove="onRemove"
/>
<Upload
accept="application/pdf"
tip="Only PDF"
:before-upload="onBeforePdfUpload"
v-model:fileList="pdfList"
@change="onChange"
@remove="onRemove"
/>
</Space>
</template>自定义上传行为
Show Code
vue
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
import { useMessage } from 'vue-amazing-ui'
import type { UploadFileType } from 'vue-amazing-ui'
const message = useMessage()
const fileList = ref<UploadFileType[]>([
{
name: '1.jpg',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/1.jpg'
},
{
name: 'Markdown.pdf',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/Markdown.pdf'
}
])
function onCustomRequest(file: File) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 模拟接口调用返回 name 和 url
let res: { name: string; url: string }
if (file.type === 'application/pdf') {
res = {
name: 'Markdown.pdf',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/Markdown.pdf'
}
} else {
res = {
name: '1.jpg',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/1.jpg'
}
}
if (res) {
resolve(res)
} else {
reject('upload request fail ...')
}
}, 1000)
})
}
function onChange(files: UploadFileType[]) {
console.log('change', files)
}
function onRemove(file: UploadFileType) {
console.log('remove', file)
}
function onSuccess(file: UploadFileType) {
console.log('upload success', file)
message.success('upload success')
}
function onError(err: unknown) {
console.log('upload error', err)
message.error(err instanceof Error ? err.message : String(err))
}
</script>
<template>
<Upload
multiple
upload-mode="custom"
:custom-request="onCustomRequest"
v-model:fileList="fileList"
@change="onChange"
@remove="onRemove"
@success="onSuccess"
@error="onError"
/>
</template>自定义分片上传
Show Code
vue
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
import { useMessage } from 'vue-amazing-ui'
import type { UploadFileType } from 'vue-amazing-ui'
const message = useMessage()
const fileList = ref<UploadFileType[]>([
{
name: '1.jpg',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/1.jpg'
},
{
name: 'Markdown.pdf',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/Markdown.pdf'
}
])
import { sliceFile } from './sliceFile'
function onCustomSliceUpload(file: File) {
return new Promise((resolve, reject) => {
console.time('sliceFile')
sliceFile(file).then((chunks) => {
console.log('chunks', chunks)
console.timeEnd('sliceFile')
setTimeout(() => {
// 模拟接口调用返回 name 和 url
let res: { name: string; url: string }
if (file.type === 'application/pdf') {
res = {
name: 'Markdown.pdf',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/Markdown.pdf'
}
} else {
res = {
name: '1.jpg',
url: 'https://cdn.jsdelivr.net/gh/themusecatcher/resources@0.1.2/1.jpg'
}
}
if (res) {
resolve(res)
} else {
reject('upload request fail ...')
}
}, 1000)
})
})
}
function onChange(files: UploadFileType[]) {
console.log('change', files)
}
function onRemove(file: UploadFileType) {
console.log('remove', file)
}
function onSuccess(file: UploadFileType) {
console.log('upload success', file)
message.success('upload success')
}
function onError(err: unknown) {
console.log('upload error', err)
message.error(err instanceof Error ? err.message : String(err))
}
</script>
<template>
<Upload
upload-mode="custom"
:custom-request="onCustomSliceUpload"
v-model:fileList="fileList"
@change="onChange"
@remove="onRemove"
@success="onSuccess"
@error="onError"
/>
</template>sliceFile Source Code
ts
const CHUNK_SIZE = 1024 * 1024 * 5 // 5MB 分片大小
// navigator.hardwareConcurrency 返回用户计算机上可用于运行线程的逻辑处理器数量
const THREAD_COUNT = navigator.hardwareConcurrency || 4 // 并发执行的线程数
export function sliceFile(file: File) {
return new Promise((resolve, reject) => {
const chunkCount = Math.ceil(file.size / CHUNK_SIZE) // 分片数量
const threadChunkCount = Math.ceil(chunkCount / THREAD_COUNT) // 每个线程分配的分片数量
const result: any[] = []
let finishCount = 0
for (let i = 0; i < THREAD_COUNT; i++) {
// 依次创建 web worker 线程
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' }) // // module 为了线程内部可以导入其他模块
const start = i * threadChunkCount
const end = Math.min(start + threadChunkCount, chunkCount)
worker.postMessage({
file,
start,
end,
CHUNK_SIZE
})
worker.onmessage = (e: MessageEvent) => {
worker.terminate()
result[i] = e.data
finishCount++
if (finishCount === THREAD_COUNT) {
// 所有线程均完成时返回结果
resolve(result.flat())
}
}
worker.onerror = (err) => {
worker.terminate()
reject(err)
}
}
})
}worker Source Code
ts
import SparkMD5 from 'spark-md5'
// 创建分片
function createFileChunk(file: File, index: number, chunkSize: number) {
return new Promise((resolve, reject) => {
const start = index * chunkSize
const end = Math.min(file.size, start + chunkSize)
const spark = new SparkMD5.ArrayBuffer() // https://github.com/satazor/js-spark-md5
const fileReader = new FileReader()
const blob = file.slice(start, end)
fileReader.onload = (e: ProgressEvent<FileReader>) => {
spark.append(e.target?.result as ArrayBuffer)
resolve({
start,
end,
index,
hash: spark.end(),
blob
})
}
fileReader.onerror = (e) => {
reject(new Error(`文件读取过程中发生错误: ${e}`))
}
fileReader.readAsArrayBuffer(blob)
})
}
// web worker 通信
onmessage = async (e: MessageEvent) => {
const { file, start, end, CHUNK_SIZE } = e.data
const result = []
for (let i = start; i < end; i++) {
const chunkPromise = createFileChunk(file, i, CHUNK_SIZE)
result.push(chunkPromise)
}
const chunks = await Promise.all(result)
postMessage(chunks)
}APIs
Upload Props
| 名称 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| accept | 接受上传的文件类型,与 <input type="file" /> 的 accept 属性一致,参考 input accept Attribute;拖拽上传时同样生效,不符合的文件通过 reject 事件回传 | string | '*' |
| multiple | 是否支持多选文件,开启后可选择多个文件;拖拽上传时同样生效,关闭时只取第一个文件 | boolean | false |
| maxCount | 限制上传数量。当为 1 时,始终用最新上传的文件代替当前文件(占满后仍显示上传入口) | number | undefined |
| tip | 上传描述文字 | string | 'Upload' |
| fit | 预览图片缩放规则,参考 object-fit,仅当上传文件为图片时生效 | 'fill' | 'contain' | 'cover' | 'none' | 'scale-down' | 'contain' |
| draggable | 是否支持拖拽上传,开启后可拖拽文件到选择框上传 | boolean | true |
| disabled | 是否禁用,只能预览,不能删除和上传 | boolean | false |
| spaceProps | Space 组件属性配置,参考 Space Props,用于配置多个文件时的排列方式 | SpaceProps | {} |
| spinProps | Spin 组件属性配置,参考 Spin Props,用于配置上传中样式 | SpinProps | {} |
| imageProps | Image 组件属性配置,参考 Image Props,用于配置图片预览 | ImageProps | {} |
| beforeUpload | 上传文件之前的钩子,参数为上传的文件,返回 false 则停止上传,返回 true 开始上传;支持返回一个 Promise 对象(如服务端校验等),Promise 对象 reject 时停止上传,resolve 时开始上传;通常用来校验用户上传的文件格式和大小 | (file: File) => boolean | void | Promise<unknown> | () => true |
| uploadMode | 上传文件的方式 | 'base64' | 'custom' | 'base64' |
| customRequest | 自定义上传行为,只有 uploadMode: custom 时,才会使用 customRequest 自定义上传行为;未配置时默认返回空结果,避免调用 .then 报错 | (file: File) => Promise<UploadFileType> | () => Promise.resolve({ url: '' }) |
| fileList v-model | 已上传的文件列表;传入即受控,不传时组件自持(未传 defaultFileList 时为空列表) | UploadFileType[] | undefined |
| defaultFileList | 非受控初始值,未绑 v-model:fileList 时生效 | UploadFileType[] | undefined |
UploadFileType Type
| 名称 | 说明 | 类型 |
|---|---|---|
| name? | 文件名 | string |
| url | 文件地址 | string |
| [propName: string] | 允许携带任意自定义字段,会随原始文件对象透传 | any |
Slots
| 名称 | 说明 | 用法 |
|---|---|---|
| tip | 自定义上传描述文字 | v-slot:tip |
Events
| 名称 | 说明 | 类型 |
|---|---|---|
| drop | 当文件被拖入上传区域时的回调 | (e: DragEvent) => void |
| reject | 拖拽的文件不符合 accept 类型时的回调,参数为被拒绝的文件列表 | (files: File[]) => void |
| change | 上传文件改变时的回调 | (files: UploadFileType[]) => void |
| preview | 点击预览时的回调 | (file: UploadFileType) => void |
| remove | 点击移除文件时的回调 | (file: UploadFileType) => void |
| success | 上传成功时的回调 | (file: UploadFileType, files: UploadFileType[]) => void |
| error | 上传失败时的回调 | (error: any) => void |
