Ant Design Vue 4文件/图片自定义上传及和阿里OSS对接

前言

本文使用Ant Design Vue 4进行自定义文件上传的操作,文件上传使用的是阿里OSS。

单图片上传

ImgUploadSingle.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<template>
<a-upload
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:customRequest="customRequest"
:before-upload="beforeUpload"
@change="handleChange"
>
<img v-if="imageUrl" :src="imageUrl" alt="avatar" />
<div v-else>
<loading-outlined v-if="loading"></loading-outlined>
<plus-outlined v-else></plus-outlined>
<div class="ant-upload-text">上传</div>
</div>
</a-upload>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { PlusOutlined, LoadingOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
const model = defineModel()
import { ossUploadFile } from '@/assets/api/alioss_upload'

const loading = ref<boolean>(false)
const imageUrl = ref<string>('')

watch(model, () => {
if (model.value) {
imageUrl.value = model.value.toString()
}
})

async function customRequest(options: any) {
const formData = new FormData()
formData.append('file', options.file) // 这里的 'file' 是后端接收文件的字段名
let uploadFile = options.file
let result = await ossUploadFile(uploadFile)
if (result) {
options.onSuccess(result.url)
} else {
options.onError('上传失败')
}
}

const handleChange = (info: any) => {
if (info.file.status === 'uploading') {
console.info(info.event || 0)
loading.value = true
return
}
if (info.file.status === 'done') {
model.value = info.file.response
}
if (info.file.status === 'error') {
loading.value = false
message.error('上传失败')
}
}

const beforeUpload = (file: any) => {
console.info(file)
console.info(file.type)
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
if (!isJpgOrPng) {
message.error('只能上传JPG/PNG图片!')
}
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
message.error('图片必须小于 2MB!')
}
return isJpgOrPng && isLt2M
}
</script>
<style scoped>
.avatar-uploader > .ant-upload {
width: 128px;
height: 128px;
}
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;
}
.ant-upload-select-picture-card {
overflow: hidden;
}
.ant-upload-select-picture-card img {
width: 96px;
border-radius: 8px;
}

.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
</style>

其中

自定义上传要设置customRequest

上传状态的变化和change事件的状态是完全对应的

开始上传

开始上传的时候,返回的状态是info.file.status === 'uploading'

上传进度

每次调用options.onProgress(80)的时候,返回的状态依旧是uploading,同时可以获取进度

1
console.info(info.event || 0)

上传成功

上传成功的话,返回的状态是doneoptions.onSuccess('https://www.psvmc.cn/head.jpg')传入的参数可以通过下面的方式获取

1
console.info(info.file.response)

封面图我们可以获取文件的Base64,也可以直接使用成功传入的地址

1
2
3
4
5
6
7
8
9
10
function getBase64(img: Blob, callback: (base64Url: string) => void) {
const reader = new FileReader()
reader.addEventListener('load', () => callback(reader.result as string))
reader.readAsDataURL(img)
}

getBase64(info.file.originFileObj, (base64Url: string) => {
imageUrl.value = base64Url
loading.value = false
})

上传失败

上传失败返回的状态是erroroptions.onError('上传失败')传入的参数可以通过下面的方式获取

1
console.info(info.file.error)

多图片上传

ImgUploadMuti.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
<template>
<a-flex gap="middle">
<div v-for="(imgUrl, index) in imageUrlArr" :key="index" class="img_outer">
<img class="zimg" :src="imgUrl" alt="avatar" />
<div class="del_outer">
<a-button size="small" @click="delItemClick(index)">删除</a-button>
</div>
</div>

<a-upload
v-show="imageUrlArr.length < props.maxNum"
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:customRequest="customRequest"
:before-upload="beforeUpload"
@change="handleChange"
>
<div>
<loading-outlined v-if="loading"></loading-outlined>
<plus-outlined v-else></plus-outlined>
<div class="ant-upload-text">上传</div>
</div>
</a-upload>
</a-flex>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { PlusOutlined, LoadingOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
const model = defineModel()
import { ossUploadFile } from '@/assets/api/alioss_upload'

const props = defineProps({
maxNum: { type: Number, default: 3 }
})

const loading = ref<boolean>(false)
const imageUrlArr = ref<string[]>([])

watch(model, () => {
if (model.value) {
imageUrlArr.value = model.value.toString().split(',')
}
})

function delItemClick(index: number) {
imageUrlArr.value.splice(index, 1)
model.value = imageUrlArr.value.join(',')
}

async function customRequest(options: any) {
const formData = new FormData()
formData.append('file', options.file) // 这里的 'file' 是后端接收文件的字段名
let uploadFile = options.file
let result = await ossUploadFile(uploadFile)
if (result) {
options.onSuccess(result.url)
} else {
options.onError('上传失败')
}
}

const handleChange = (info: any) => {
if (info.file.status === 'uploading') {
console.info(info.event || 0)
loading.value = true
return
}
if (info.file.status === 'done') {
loading.value = false
imageUrlArr.value.push(info.file.response)
model.value = imageUrlArr.value.join(',')
}
if (info.file.status === 'error') {
loading.value = false
message.error('上传失败')
}
}

const beforeUpload = (file: any) => {
console.info(file)
console.info(file.type)
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'
if (!isJpgOrPng) {
message.error('只能上传JPG/PNG图片!')
}
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
message.error('图片必须小于 2MB!')
}
return isJpgOrPng && isLt2M
}
</script>
<style scoped>
.img_outer {
position: relative;
}

.img_outer .del_outer {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: none;
background: #00000033;
align-items: flex-start;
justify-content: flex-end;
padding: 4px;
}

.img_outer:hover .del_outer {
display: flex;
}

.zimg {
height: 100px;
}
.avatar-uploader > .ant-upload {
width: 128px;
height: 128px;
}
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;
}
.ant-upload-select-picture-card {
overflow: hidden;
}
.ant-upload-select-picture-card img {
width: 96px;
border-radius: 8px;
}

.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
</style>

单视频上传

VideoUploadSingle.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<template>
<div class="video_upload_outer">
<div v-if="videoUrl" class="video_outer">
<video controls>
<source :src="videoUrl" type="video/mp4" />
您的浏览器不支持 video 标签。
</video>
</div>
<a-upload
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
:customRequest="customRequest"
:before-upload="beforeUpload"
@change="handleChange"
>
<div>
<loading-outlined v-if="loading"></loading-outlined>
<plus-outlined></plus-outlined>
<div class="ant-upload-text">上传</div>
</div>
</a-upload>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { PlusOutlined, LoadingOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
const model = defineModel()
import { ossUploadFile } from '@/assets/api/alioss_upload'

const loading = ref<boolean>(false)
const videoUrl = ref<string>('')

watch(model, () => {
if (model.value) {
videoUrl.value = model.value.toString()
}
})

async function customRequest(options: any) {
const formData = new FormData()
formData.append('file', options.file) // 这里的 'file' 是后端接收文件的字段名
let uploadFile = options.file
let result = await ossUploadFile(uploadFile)
if (result) {
options.onSuccess(result.url)
} else {
options.onError('上传失败')
}
}

const handleChange = (info: any) => {
if (info.file.status === 'uploading') {
console.info(info.event || 0)
loading.value = true
return
}
if (info.file.status === 'done') {
loading.value = false
model.value = info.file.response
}
if (info.file.status === 'error') {
loading.value = false
message.error('上传失败')
}
}

const beforeUpload = (file: any) => {
const isVideo =
file.type === 'video/mp4' || file.name.endsWith('.mp4') || file.name.endsWith('.flv')
if (!isVideo) {
message.error('只能上传MP4/FLV格式的视频!')
}
const isLt200M = file.size / 1024 / 1024 < 200
if (!isLt200M) {
message.error('图片必须小于 200MB!')
}
return isVideo && isLt200M
}
</script>
<style scoped>
.video_upload_outer {
display: flex;
gap: 10px;
position: relative;
height: 300px;
width: 100%;
}

.video_outer {
position: relative;
width: 400px;
height: 100%;
flex: none;
}

video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover; /* 使视频填充容器并保持比例 */
}
.avatar-uploader > .ant-upload {
width: 300px;
height: 200px;
}
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;
}
.ant-upload-select-picture-card {
overflow: hidden;
}
.ant-upload-select-picture-card img {
width: 96px;
height: 96px;
border-radius: 8px;
}

.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
</style>

阿里OSS上传

npm安装

1
2
npm install ali-oss --save
npm install uuid

示例

这里通过临时凭证进行文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import { apiCustomGetSts } from '@/assets/api/common_api.js'
import { v4 as uuidv4 } from 'uuid'

function getDateStr() {
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth() + 1
const day = now.getDate()
const formattedMonth = month.toString().padStart(2, '0')
const formattedDay = day.toString().padStart(2, '0')
return `${year}-${formattedMonth}-${formattedDay}`
}

// 引入ali-oss
import OSS from 'ali-oss'
export const ossUploadFile = async (file) => {
let fileName = file.name
fileName = fileName.replace(/[^a-zA-Z0-9_.-]/g, '')
let res = await apiCustomGetSts()
if (res.code === 0) {
let { accessKeyId, accessKeySecret, securityToken } = res.obj
let client = new OSS({
region: 'oss-cn-qingdao', // bucket所在的区域, 默认oss-cn-hangzhou
secure: true, // secure: 配合region使用,如果指定了secure为true,则使用HTTPS访问
accessKeyId: accessKeyId, // 通过阿里云控制台创建的AccessKey
accessKeySecret: accessKeySecret, // 通过阿里云控制台创建的AccessSecret
stsToken: securityToken,
bucket: 'cxzfile', // 通过控制台或PutBucket创建的bucket
refreshSTSToken: async () => {
// 向您搭建的STS服务获取临时访问凭证。
const result2 = await apiCustomGetSts()
if (result2.code === 0) {
let { accessKeyId, accessKeySecret, securityToken } = result2.obj
return {
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
stsToken: securityToken
}
}
}
})
try {
let uuidStr = uuidv4()
// ObjName为文件名字,可以只写名字,就直接储存在 bucket 的根路径,如需放在文件夹下面直接在文件名前面加上文件夹名称
return await client.put(`admin_imgs/${getDateStr()}/${uuidStr}-${fileName}`, file)
} catch (e) {
console.log(e)
}
}
return null
}