Skip to content

Commit 490bb90

Browse files
committed
基础设施:增加前端直连上传文件到S3服务的功能
1 parent 64cfcbf commit 490bb90

File tree

12 files changed

+152
-41
lines changed

12 files changed

+152
-41
lines changed

.env.dev

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ VITE_DEV=true
77
VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
88
# VITE_BASE_URL='http://dofast.demo.huizhizao.vip:20001'
99

10+
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
11+
VITE_UPLOAD_TYPE=server
1012
# 上传路径
1113
VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'
1214

.env.local

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ VITE_DEV=true
66
# 请求路径
77
VITE_BASE_URL='http://localhost:48080'
88

9+
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
10+
VITE_UPLOAD_TYPE=server
911
# 上传路径
1012
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
1113

.env.prod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ VITE_DEV=false
66
# 请求路径
77
VITE_BASE_URL='http://localhost:48080'
88

9+
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
10+
VITE_UPLOAD_TYPE=server
911
# 上传路径
1012
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
1113

.env.stage

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ VITE_DEV=false
66
# 请求路径
77
VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
88

9+
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
10+
VITE_UPLOAD_TYPE=server
911
# 上传路径
1012
VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'
1113

.env.test

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ VITE_DEV=false
66
# 请求路径
77
VITE_BASE_URL='http://localhost:48080'
88

9+
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
10+
VITE_UPLOAD_TYPE=server
911
# 上传路径
1012
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'
1113

src/api/infra/file/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ export interface FilePageReqVO extends PageParam {
66
createTime?: Date[]
77
}
88

9+
// 文件预签名地址 Response VO
10+
export interface FilePresignedUrlRespVO {
11+
// 文件配置编号
12+
configId: number
13+
// 文件预签名地址
14+
url: string
15+
}
16+
917
// 查询文件列表
1018
export const getFilePage = (params: FilePageReqVO) => {
1119
return request.get({ url: '/infra/file/page', params })
@@ -15,3 +23,16 @@ export const getFilePage = (params: FilePageReqVO) => {
1523
export const deleteFile = (id: number) => {
1624
return request.delete({ url: '/infra/file/delete?id=' + id })
1725
}
26+
27+
// 获取文件预签名地址
28+
export const getFilePresignedUrl = (fileName: string) => {
29+
return request.get<FilePresignedUrlRespVO>({
30+
url: '/infra/file/presigned-url',
31+
params: { fileName }
32+
})
33+
}
34+
35+
// 创建文件
36+
export const createFile = (data: any) => {
37+
return request.post({ url: '/infra/file/create', data })
38+
}

src/components/UploadFile/src/UploadFile.vue

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33
<el-upload
44
ref="uploadRef"
55
v-model:file-list="fileList"
6-
:action="updateUrl"
6+
:action="uploadUrl"
77
:auto-upload="autoUpload"
88
:before-upload="beforeUpload"
99
:drag="drag"
10-
:headers="uploadHeaders"
1110
:limit="props.limit"
1211
:multiple="props.limit > 1"
1312
:on-error="excelUploadError"
@@ -16,6 +15,7 @@
1615
:on-remove="handleRemove"
1716
:on-success="handleFileSuccess"
1817
:show-file-list="true"
18+
:http-request="httpRequest"
1919
class="upload-file-uploader"
2020
name="file"
2121
>
@@ -36,9 +36,10 @@
3636
</template>
3737
<script lang="ts" setup>
3838
import { propTypes } from '@/utils/propTypes'
39-
import { getAccessToken, getTenantId } from '@/utils/auth'
4039
import type { UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus'
4140
import { isString } from '@/utils/is'
41+
import { useUpload } from '@/components/UploadFile/src/useUpload'
42+
import { UploadFile } from 'element-plus/es/components/upload/src/upload'
4243
4344
defineOptions({ name: 'UploadFile' })
4445
@@ -48,7 +49,6 @@ const emit = defineEmits(['update:modelValue'])
4849
const props = defineProps({
4950
modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
5051
title: propTypes.string.def('文件上传'),
51-
updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
5252
fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // 文件类型, 例如['png', 'jpg', 'jpeg']
5353
fileSize: propTypes.number.def(5), // 大小限制(MB)
5454
limit: propTypes.number.def(5), // 数量限制
@@ -62,10 +62,8 @@ const uploadRef = ref<UploadInstance>()
6262
const uploadList = ref<UploadUserFile[]>([])
6363
const fileList = ref<UploadUserFile[]>([])
6464
const uploadNumber = ref<number>(0)
65-
const uploadHeaders = ref({
66-
Authorization: 'Bearer ' + getAccessToken(),
67-
'tenant-id': getTenantId()
68-
})
65+
66+
const { uploadUrl, httpRequest } = useUpload()
6967
7068
// 文件上传之前判断
7169
const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
@@ -120,10 +118,10 @@ const excelUploadError: UploadProps['onError'] = (): void => {
120118
message.error('导入数据失败,请您重新上传!')
121119
}
122120
// 删除上传文件
123-
const handleRemove = (file) => {
124-
const findex = fileList.value.map((f) => f.name).indexOf(file.name)
125-
if (findex > -1) {
126-
fileList.value.splice(findex, 1)
121+
const handleRemove = (file: UploadFile) => {
122+
const index = fileList.value.map((f) => f.name).indexOf(file.name)
123+
if (index > -1) {
124+
fileList.value.splice(index, 1)
127125
emitUpdateModelValue()
128126
}
129127
}

src/components/UploadFile/src/UploadImg.vue

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
<el-upload
44
:id="uuid"
55
:accept="fileType.join(',')"
6-
:action="updateUrl"
6+
:action="uploadUrl"
77
:before-upload="beforeUpload"
88
:class="['upload', drag ? 'no-border' : '']"
99
:drag="drag"
10-
:headers="uploadHeaders"
1110
:multiple="false"
1211
:on-error="uploadError"
1312
:on-success="uploadSuccess"
1413
:show-file-list="false"
14+
:http-request="httpRequest"
1515
>
1616
<template v-if="modelValue">
1717
<img :src="modelValue" class="upload-image" />
@@ -50,8 +50,8 @@ import type { UploadProps } from 'element-plus'
5050
5151
import { generateUUID } from '@/utils'
5252
import { propTypes } from '@/utils/propTypes'
53-
import { getAccessToken, getTenantId } from '@/utils/auth'
5453
import { createImageViewer } from '@/components/ImageViewer'
54+
import { useUpload } from '@/components/UploadFile/src/useUpload'
5555
5656
defineOptions({ name: 'UploadImg' })
5757
@@ -70,7 +70,6 @@ type FileTypes =
7070
// 接受父组件参数
7171
const props = defineProps({
7272
modelValue: propTypes.string.def(''),
73-
updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
7473
drag: propTypes.bool.def(true), // 是否支持拖拽上传 ==> 非必传(默认为 true)
7574
disabled: propTypes.bool.def(false), // 是否禁用上传组件 ==> 非必传(默认为 false)
7675
fileSize: propTypes.number.def(5), // 图片大小限制 ==> 非必传(默认为 5M)
@@ -101,10 +100,7 @@ const deleteImg = () => {
101100
emit('update:modelValue', '')
102101
}
103102
104-
const uploadHeaders = ref({
105-
Authorization: 'Bearer ' + getAccessToken(),
106-
'tenant-id': getTenantId()
107-
})
103+
const { uploadUrl, httpRequest } = useUpload()
108104
109105
const editImg = () => {
110106
const dom = document.querySelector(`#${uuid.value} .el-upload__input`)

src/components/UploadFile/src/UploadImgs.vue

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
<el-upload
44
v-model:file-list="fileList"
55
:accept="fileType.join(',')"
6-
:action="updateUrl"
6+
:action="uploadUrl"
77
:before-upload="beforeUpload"
88
:class="['upload', drag ? 'no-border' : '']"
99
:drag="drag"
10-
:headers="uploadHeaders"
1110
:limit="limit"
1211
:multiple="true"
1312
:on-error="uploadError"
1413
:on-exceed="handleExceed"
1514
:on-success="uploadSuccess"
15+
:http-request="httpRequest"
1616
list-type="picture-card"
1717
>
1818
<div class="upload-empty">
@@ -50,7 +50,7 @@ import type { UploadFile, UploadProps, UploadUserFile } from 'element-plus'
5050
import { ElNotification } from 'element-plus'
5151
5252
import { propTypes } from '@/utils/propTypes'
53-
import { getAccessToken, getTenantId } from '@/utils/auth'
53+
import { useUpload } from '@/components/UploadFile/src/useUpload'
5454
5555
defineOptions({ name: 'UploadImgs' })
5656
@@ -70,7 +70,6 @@ type FileTypes =
7070
7171
const props = defineProps({
7272
modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
73-
updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
7473
drag: propTypes.bool.def(true), // 是否支持拖拽上传 ==> 非必传(默认为 true)
7574
disabled: propTypes.bool.def(false), // 是否禁用上传组件 ==> 非必传(默认为 false)
7675
limit: propTypes.number.def(5), // 最大图片上传数 ==> 非必传(默认为 5张)
@@ -81,10 +80,7 @@ const props = defineProps({
8180
borderradius: propTypes.string.def('8px') // 组件边框圆角 ==> 非必传(默认为 8px)
8281
})
8382
84-
const uploadHeaders = ref({
85-
Authorization: 'Bearer ' + getAccessToken(),
86-
'tenant-id': getTenantId()
87-
})
83+
const { uploadUrl, httpRequest } = useUpload()
8884
8985
const fileList = ref<UploadUserFile[]>([])
9086
const uploadNumber = ref<number>(0)
@@ -121,7 +117,6 @@ const emit = defineEmits<UploadEmits>()
121117
const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => {
122118
message.success('上传成功')
123119
// 删除自身
124-
debugger
125120
const index = fileList.value.findIndex((item) => item.response?.data === res.data)
126121
fileList.value.splice(index, 1)
127122
uploadList.value.push({ name: res.data, url: res.data })
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { getAccessToken, getTenantId } from '@/utils/auth'
2+
import * as FileApi from '@/api/infra/file'
3+
import CryptoJS from 'crypto-js'
4+
import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
5+
import { ajaxUpload } from 'element-plus/es/components/upload/src/ajax'
6+
import axios from 'axios'
7+
8+
export const useUpload = () => {
9+
// 后端上传地址
10+
const uploadUrl = import.meta.env.VITE_UPLOAD_URL
11+
// 是否使用前端直连上传
12+
const isClientUpload = UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE
13+
// 重写ElUpload上传方法
14+
const httpRequest = async (options: UploadRequestOptions) => {
15+
// 模式一:前端上传
16+
if (isClientUpload) {
17+
// 1.1 生成文件名称
18+
const fileName = await generateFileName(options.file)
19+
// 1.2 获取文件预签名地址
20+
const presignedInfo = await FileApi.getFilePresignedUrl(fileName)
21+
// 1.3 上传文件(不能使用ElUpload的ajaxUpload方法的原因:其使用的是FormData上传,Minio不支持)
22+
return axios.put(presignedInfo.url, options.file).then(() => {
23+
// 1.4. 记录文件信息到后端
24+
const fileVo = createFile(presignedInfo.configId, fileName, presignedInfo.url, options.file)
25+
// 通知成功,数据格式保持与后端上传的返回结果一致
26+
return { data: fileVo.url }
27+
})
28+
} else {
29+
// 模式二:后端上传(需要增加后端身份认证请求头)
30+
options.headers['Authorization'] = 'Bearer ' + getAccessToken()
31+
options.headers['tenant-id'] = getTenantId()
32+
// 使用ElUpload的上传方法
33+
return ajaxUpload(options)
34+
}
35+
}
36+
37+
return {
38+
uploadUrl,
39+
httpRequest
40+
}
41+
}
42+
43+
/**
44+
* 创建文件信息
45+
* @param configId 文件配置编号
46+
* @param name 文件名称
47+
* @param url 文件地址
48+
* @param file 文件
49+
*/
50+
function createFile(configId: number, name: string, url: string, file: UploadRawFile) {
51+
const fileVo = {
52+
configId: configId,
53+
path: name,
54+
// 移除预签名参数:参数只在上传时有用,查看时不需要
55+
url: url.substring(0, url.indexOf('?')),
56+
name: file.name,
57+
type: file.type,
58+
size: file.size
59+
}
60+
FileApi.createFile(fileVo)
61+
return fileVo
62+
}
63+
64+
/**
65+
* 生成文件名称(使用算法SHA256)
66+
* @param file 要上传的文件
67+
*/
68+
async function generateFileName(file: UploadRawFile) {
69+
// 读取文件内容
70+
const data = await file.arrayBuffer()
71+
const wordArray = CryptoJS.lib.WordArray.create(data)
72+
// 计算SHA256
73+
const sha256 = CryptoJS.SHA256(wordArray).toString()
74+
// 拼接后缀
75+
const ext = file.name.substring(file.name.lastIndexOf('.'))
76+
return `${sha256}${ext}`
77+
}
78+
79+
/**
80+
* 上传类型
81+
*/
82+
enum UPLOAD_TYPE {
83+
// 客户端直接上传(只支持S3服务)
84+
CLIENT = 'client',
85+
// 客户端发送到后端上传
86+
SERVER = 'server'
87+
}

0 commit comments

Comments
 (0)