基础设施:增加前端直连上传文件到S3服务的功能

This commit is contained in:
owen 2024-02-04 19:41:48 +08:00
parent 64cfcbf6e0
commit 490bb901e1
12 changed files with 152 additions and 41 deletions

View File

@ -7,6 +7,8 @@ VITE_DEV=true
VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn' VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
# VITE_BASE_URL='http://dofast.demo.huizhizao.vip:20001' # VITE_BASE_URL='http://dofast.demo.huizhizao.vip:20001'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server
# 上传路径 # 上传路径
VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload' VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'

View File

@ -6,6 +6,8 @@ VITE_DEV=true
# 请求路径 # 请求路径
VITE_BASE_URL='http://localhost:48080' VITE_BASE_URL='http://localhost:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server
# 上传路径 # 上传路径
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'

View File

@ -6,6 +6,8 @@ VITE_DEV=false
# 请求路径 # 请求路径
VITE_BASE_URL='http://localhost:48080' VITE_BASE_URL='http://localhost:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server
# 上传路径 # 上传路径
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'

View File

@ -6,6 +6,8 @@ VITE_DEV=false
# 请求路径 # 请求路径
VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn' VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server
# 上传路径 # 上传路径
VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload' VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'

View File

@ -6,6 +6,8 @@ VITE_DEV=false
# 请求路径 # 请求路径
VITE_BASE_URL='http://localhost:48080' VITE_BASE_URL='http://localhost:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传仅支持S3服务
VITE_UPLOAD_TYPE=server
# 上传路径 # 上传路径
VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload'

View File

@ -6,6 +6,14 @@ export interface FilePageReqVO extends PageParam {
createTime?: Date[] createTime?: Date[]
} }
// 文件预签名地址 Response VO
export interface FilePresignedUrlRespVO {
// 文件配置编号
configId: number
// 文件预签名地址
url: string
}
// 查询文件列表 // 查询文件列表
export const getFilePage = (params: FilePageReqVO) => { export const getFilePage = (params: FilePageReqVO) => {
return request.get({ url: '/infra/file/page', params }) return request.get({ url: '/infra/file/page', params })
@ -15,3 +23,16 @@ export const getFilePage = (params: FilePageReqVO) => {
export const deleteFile = (id: number) => { export const deleteFile = (id: number) => {
return request.delete({ url: '/infra/file/delete?id=' + id }) return request.delete({ url: '/infra/file/delete?id=' + id })
} }
// 获取文件预签名地址
export const getFilePresignedUrl = (fileName: string) => {
return request.get<FilePresignedUrlRespVO>({
url: '/infra/file/presigned-url',
params: { fileName }
})
}
// 创建文件
export const createFile = (data: any) => {
return request.post({ url: '/infra/file/create', data })
}

View File

@ -3,11 +3,10 @@
<el-upload <el-upload
ref="uploadRef" ref="uploadRef"
v-model:file-list="fileList" v-model:file-list="fileList"
:action="updateUrl" :action="uploadUrl"
:auto-upload="autoUpload" :auto-upload="autoUpload"
:before-upload="beforeUpload" :before-upload="beforeUpload"
:drag="drag" :drag="drag"
:headers="uploadHeaders"
:limit="props.limit" :limit="props.limit"
:multiple="props.limit > 1" :multiple="props.limit > 1"
:on-error="excelUploadError" :on-error="excelUploadError"
@ -16,6 +15,7 @@
:on-remove="handleRemove" :on-remove="handleRemove"
:on-success="handleFileSuccess" :on-success="handleFileSuccess"
:show-file-list="true" :show-file-list="true"
:http-request="httpRequest"
class="upload-file-uploader" class="upload-file-uploader"
name="file" name="file"
> >
@ -36,9 +36,10 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
import { getAccessToken, getTenantId } from '@/utils/auth'
import type { UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus' import type { UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus'
import { isString } from '@/utils/is' import { isString } from '@/utils/is'
import { useUpload } from '@/components/UploadFile/src/useUpload'
import { UploadFile } from 'element-plus/es/components/upload/src/upload'
defineOptions({ name: 'UploadFile' }) defineOptions({ name: 'UploadFile' })
@ -48,7 +49,6 @@ const emit = defineEmits(['update:modelValue'])
const props = defineProps({ const props = defineProps({
modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired, modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
title: propTypes.string.def('文件上传'), title: propTypes.string.def('文件上传'),
updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // , ['png', 'jpg', 'jpeg'] fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // , ['png', 'jpg', 'jpeg']
fileSize: propTypes.number.def(5), // (MB) fileSize: propTypes.number.def(5), // (MB)
limit: propTypes.number.def(5), // limit: propTypes.number.def(5), //
@ -62,10 +62,8 @@ const uploadRef = ref<UploadInstance>()
const uploadList = ref<UploadUserFile[]>([]) const uploadList = ref<UploadUserFile[]>([])
const fileList = ref<UploadUserFile[]>([]) const fileList = ref<UploadUserFile[]>([])
const uploadNumber = ref<number>(0) const uploadNumber = ref<number>(0)
const uploadHeaders = ref({
Authorization: 'Bearer ' + getAccessToken(), const { uploadUrl, httpRequest } = useUpload()
'tenant-id': getTenantId()
})
// //
const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => { const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
@ -120,10 +118,10 @@ const excelUploadError: UploadProps['onError'] = (): void => {
message.error('导入数据失败,请您重新上传!') message.error('导入数据失败,请您重新上传!')
} }
// //
const handleRemove = (file) => { const handleRemove = (file: UploadFile) => {
const findex = fileList.value.map((f) => f.name).indexOf(file.name) const index = fileList.value.map((f) => f.name).indexOf(file.name)
if (findex > -1) { if (index > -1) {
fileList.value.splice(findex, 1) fileList.value.splice(index, 1)
emitUpdateModelValue() emitUpdateModelValue()
} }
} }

View File

@ -3,15 +3,15 @@
<el-upload <el-upload
:id="uuid" :id="uuid"
:accept="fileType.join(',')" :accept="fileType.join(',')"
:action="updateUrl" :action="uploadUrl"
:before-upload="beforeUpload" :before-upload="beforeUpload"
:class="['upload', drag ? 'no-border' : '']" :class="['upload', drag ? 'no-border' : '']"
:drag="drag" :drag="drag"
:headers="uploadHeaders"
:multiple="false" :multiple="false"
:on-error="uploadError" :on-error="uploadError"
:on-success="uploadSuccess" :on-success="uploadSuccess"
:show-file-list="false" :show-file-list="false"
:http-request="httpRequest"
> >
<template v-if="modelValue"> <template v-if="modelValue">
<img :src="modelValue" class="upload-image" /> <img :src="modelValue" class="upload-image" />
@ -50,8 +50,8 @@ import type { UploadProps } from 'element-plus'
import { generateUUID } from '@/utils' import { generateUUID } from '@/utils'
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
import { getAccessToken, getTenantId } from '@/utils/auth'
import { createImageViewer } from '@/components/ImageViewer' import { createImageViewer } from '@/components/ImageViewer'
import { useUpload } from '@/components/UploadFile/src/useUpload'
defineOptions({ name: 'UploadImg' }) defineOptions({ name: 'UploadImg' })
@ -70,7 +70,6 @@ type FileTypes =
// //
const props = defineProps({ const props = defineProps({
modelValue: propTypes.string.def(''), modelValue: propTypes.string.def(''),
updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
drag: propTypes.bool.def(true), // ==> true drag: propTypes.bool.def(true), // ==> true
disabled: propTypes.bool.def(false), // ==> false disabled: propTypes.bool.def(false), // ==> false
fileSize: propTypes.number.def(5), // ==> 5M fileSize: propTypes.number.def(5), // ==> 5M
@ -101,10 +100,7 @@ const deleteImg = () => {
emit('update:modelValue', '') emit('update:modelValue', '')
} }
const uploadHeaders = ref({ const { uploadUrl, httpRequest } = useUpload()
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
})
const editImg = () => { const editImg = () => {
const dom = document.querySelector(`#${uuid.value} .el-upload__input`) const dom = document.querySelector(`#${uuid.value} .el-upload__input`)

View File

@ -3,16 +3,16 @@
<el-upload <el-upload
v-model:file-list="fileList" v-model:file-list="fileList"
:accept="fileType.join(',')" :accept="fileType.join(',')"
:action="updateUrl" :action="uploadUrl"
:before-upload="beforeUpload" :before-upload="beforeUpload"
:class="['upload', drag ? 'no-border' : '']" :class="['upload', drag ? 'no-border' : '']"
:drag="drag" :drag="drag"
:headers="uploadHeaders"
:limit="limit" :limit="limit"
:multiple="true" :multiple="true"
:on-error="uploadError" :on-error="uploadError"
:on-exceed="handleExceed" :on-exceed="handleExceed"
:on-success="uploadSuccess" :on-success="uploadSuccess"
:http-request="httpRequest"
list-type="picture-card" list-type="picture-card"
> >
<div class="upload-empty"> <div class="upload-empty">
@ -50,7 +50,7 @@ import type { UploadFile, UploadProps, UploadUserFile } from 'element-plus'
import { ElNotification } from 'element-plus' import { ElNotification } from 'element-plus'
import { propTypes } from '@/utils/propTypes' import { propTypes } from '@/utils/propTypes'
import { getAccessToken, getTenantId } from '@/utils/auth' import { useUpload } from '@/components/UploadFile/src/useUpload'
defineOptions({ name: 'UploadImgs' }) defineOptions({ name: 'UploadImgs' })
@ -70,7 +70,6 @@ type FileTypes =
const props = defineProps({ const props = defineProps({
modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired, modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
drag: propTypes.bool.def(true), // ==> true drag: propTypes.bool.def(true), // ==> true
disabled: propTypes.bool.def(false), // ==> false disabled: propTypes.bool.def(false), // ==> false
limit: propTypes.number.def(5), // ==> 5 limit: propTypes.number.def(5), // ==> 5
@ -81,10 +80,7 @@ const props = defineProps({
borderradius: propTypes.string.def('8px') // ==> 8px borderradius: propTypes.string.def('8px') // ==> 8px
}) })
const uploadHeaders = ref({ const { uploadUrl, httpRequest } = useUpload()
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
})
const fileList = ref<UploadUserFile[]>([]) const fileList = ref<UploadUserFile[]>([])
const uploadNumber = ref<number>(0) const uploadNumber = ref<number>(0)
@ -121,7 +117,6 @@ const emit = defineEmits<UploadEmits>()
const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => { const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => {
message.success('上传成功') message.success('上传成功')
// //
debugger
const index = fileList.value.findIndex((item) => item.response?.data === res.data) const index = fileList.value.findIndex((item) => item.response?.data === res.data)
fileList.value.splice(index, 1) fileList.value.splice(index, 1)
uploadList.value.push({ name: res.data, url: res.data }) uploadList.value.push({ name: res.data, url: res.data })

View File

@ -0,0 +1,87 @@
import { getAccessToken, getTenantId } from '@/utils/auth'
import * as FileApi from '@/api/infra/file'
import CryptoJS from 'crypto-js'
import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
import { ajaxUpload } from 'element-plus/es/components/upload/src/ajax'
import axios from 'axios'
export const useUpload = () => {
// 后端上传地址
const uploadUrl = import.meta.env.VITE_UPLOAD_URL
// 是否使用前端直连上传
const isClientUpload = UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE
// 重写ElUpload上传方法
const httpRequest = async (options: UploadRequestOptions) => {
// 模式一:前端上传
if (isClientUpload) {
// 1.1 生成文件名称
const fileName = await generateFileName(options.file)
// 1.2 获取文件预签名地址
const presignedInfo = await FileApi.getFilePresignedUrl(fileName)
// 1.3 上传文件不能使用ElUpload的ajaxUpload方法的原因其使用的是FormData上传Minio不支持
return axios.put(presignedInfo.url, options.file).then(() => {
// 1.4. 记录文件信息到后端
const fileVo = createFile(presignedInfo.configId, fileName, presignedInfo.url, options.file)
// 通知成功,数据格式保持与后端上传的返回结果一致
return { data: fileVo.url }
})
} else {
// 模式二:后端上传(需要增加后端身份认证请求头)
options.headers['Authorization'] = 'Bearer ' + getAccessToken()
options.headers['tenant-id'] = getTenantId()
// 使用ElUpload的上传方法
return ajaxUpload(options)
}
}
return {
uploadUrl,
httpRequest
}
}
/**
*
* @param configId
* @param name
* @param url
* @param file
*/
function createFile(configId: number, name: string, url: string, file: UploadRawFile) {
const fileVo = {
configId: configId,
path: name,
// 移除预签名参数:参数只在上传时有用,查看时不需要
url: url.substring(0, url.indexOf('?')),
name: file.name,
type: file.type,
size: file.size
}
FileApi.createFile(fileVo)
return fileVo
}
/**
* 使SHA256
* @param file
*/
async function generateFileName(file: UploadRawFile) {
// 读取文件内容
const data = await file.arrayBuffer()
const wordArray = CryptoJS.lib.WordArray.create(data)
// 计算SHA256
const sha256 = CryptoJS.SHA256(wordArray).toString()
// 拼接后缀
const ext = file.name.substring(file.name.lastIndexOf('.'))
return `${sha256}${ext}`
}
/**
*
*/
enum UPLOAD_TYPE {
// 客户端直接上传只支持S3服务
CLIENT = 'client',
// 客户端发送到后端上传
SERVER = 'server'
}

View File

@ -3,16 +3,16 @@
<el-upload <el-upload
ref="uploadRef" ref="uploadRef"
v-model:file-list="fileList" v-model:file-list="fileList"
:action="url" :action="uploadUrl"
:auto-upload="false" :auto-upload="false"
:data="data" :data="data"
:disabled="formLoading" :disabled="formLoading"
:headers="uploadHeaders"
:limit="1" :limit="1"
:on-change="handleFileChange" :on-change="handleFileChange"
:on-error="submitFormError" :on-error="submitFormError"
:on-exceed="handleExceed" :on-exceed="handleExceed"
:on-success="submitFormSuccess" :on-success="submitFormSuccess"
:http-request="httpRequest"
accept=".jpg, .png, .gif" accept=".jpg, .png, .gif"
drag drag
> >
@ -31,7 +31,7 @@
</Dialog> </Dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { getAccessToken, getTenantId } from '@/utils/auth' import { useUpload } from '@/components/UploadFile/src/useUpload'
defineOptions({ name: 'InfraFileForm' }) defineOptions({ name: 'InfraFileForm' })
@ -40,12 +40,12 @@ const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // const dialogVisible = ref(false) //
const formLoading = ref(false) // const formLoading = ref(false) //
const url = import.meta.env.VITE_UPLOAD_URL
const uploadHeaders = ref() // Header
const fileList = ref([]) // const fileList = ref([]) //
const data = ref({ path: '' }) const data = ref({ path: '' })
const uploadRef = ref() const uploadRef = ref()
const { uploadUrl, httpRequest } = useUpload()
/** 打开弹窗 */ /** 打开弹窗 */
const open = async () => { const open = async () => {
dialogVisible.value = true dialogVisible.value = true
@ -64,11 +64,6 @@ const submitFileForm = () => {
message.error('请上传文件') message.error('请上传文件')
return return
} }
//
uploadHeaders.value = {
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
}
unref(uploadRef)?.submit() unref(uploadRef)?.submit()
} }

View File

@ -70,8 +70,17 @@
preview-teleported preview-teleported
fit="cover" fit="cover"
/> />
<el-link v-else-if="row.type.includes('pdf')" type="primary" :href="row.url" :underline="false" target="_blank">预览</el-link> <el-link
<el-link v-else type="primary" download :href="row.url" :underline="false" target="_blank">下载</el-link> v-else-if="row.type.includes('pdf')"
type="primary"
:href="row.url"
:underline="false"
target="_blank"
>预览</el-link
>
<el-link v-else type="primary" download :href="row.url" :underline="false" target="_blank"
>下载</el-link
>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column