From 490bb901e175abe1d2b4c198a7c0a0424218c6df Mon Sep 17 00:00:00 2001 From: owen <owen@evolsun.com> Date: Sun, 4 Feb 2024 19:41:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD=EF=BC=9A?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=89=8D=E7=AB=AF=E7=9B=B4=E8=BF=9E=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E6=96=87=E4=BB=B6=E5=88=B0S3=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.dev | 2 + .env.local | 2 + .env.prod | 2 + .env.stage | 2 + .env.test | 2 + src/api/infra/file/index.ts | 21 +++++ src/components/UploadFile/src/UploadFile.vue | 22 +++-- src/components/UploadFile/src/UploadImg.vue | 12 +-- src/components/UploadFile/src/UploadImgs.vue | 13 +-- src/components/UploadFile/src/useUpload.ts | 87 ++++++++++++++++++++ src/views/infra/file/FileForm.vue | 15 ++-- src/views/infra/file/index.vue | 13 ++- 12 files changed, 152 insertions(+), 41 deletions(-) create mode 100644 src/components/UploadFile/src/useUpload.ts diff --git a/.env.dev b/.env.dev index 3c41cc68..689718b2 100644 --- a/.env.dev +++ b/.env.dev @@ -7,6 +7,8 @@ VITE_DEV=true VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn' # 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' diff --git a/.env.local b/.env.local index 2eb968c4..28f37538 100644 --- a/.env.local +++ b/.env.local @@ -6,6 +6,8 @@ VITE_DEV=true # 请求路径 VITE_BASE_URL='http://localhost:48080' +# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 +VITE_UPLOAD_TYPE=server # 上传路径 VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' diff --git a/.env.prod b/.env.prod index 070b43a7..35d729c3 100644 --- a/.env.prod +++ b/.env.prod @@ -6,6 +6,8 @@ VITE_DEV=false # 请求路径 VITE_BASE_URL='http://localhost:48080' +# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 +VITE_UPLOAD_TYPE=server # 上传路径 VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' diff --git a/.env.stage b/.env.stage index c0edf340..26f9516b 100644 --- a/.env.stage +++ b/.env.stage @@ -6,6 +6,8 @@ VITE_DEV=false # 请求路径 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' diff --git a/.env.test b/.env.test index 217ac6e2..addbfb4e 100644 --- a/.env.test +++ b/.env.test @@ -6,6 +6,8 @@ VITE_DEV=false # 请求路径 VITE_BASE_URL='http://localhost:48080' +# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 +VITE_UPLOAD_TYPE=server # 上传路径 VITE_UPLOAD_URL='http://localhost:48080/admin-api/infra/file/upload' diff --git a/src/api/infra/file/index.ts b/src/api/infra/file/index.ts index f64bc0d6..7f543745 100644 --- a/src/api/infra/file/index.ts +++ b/src/api/infra/file/index.ts @@ -6,6 +6,14 @@ export interface FilePageReqVO extends PageParam { createTime?: Date[] } +// 文件预签名地址 Response VO +export interface FilePresignedUrlRespVO { + // 文件配置编号 + configId: number + // 文件预签名地址 + url: string +} + // 查询文件列表 export const getFilePage = (params: FilePageReqVO) => { return request.get({ url: '/infra/file/page', params }) @@ -15,3 +23,16 @@ export const getFilePage = (params: FilePageReqVO) => { export const deleteFile = (id: number) => { 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 }) +} diff --git a/src/components/UploadFile/src/UploadFile.vue b/src/components/UploadFile/src/UploadFile.vue index a0ef08d6..576dc054 100644 --- a/src/components/UploadFile/src/UploadFile.vue +++ b/src/components/UploadFile/src/UploadFile.vue @@ -3,11 +3,10 @@ <el-upload ref="uploadRef" v-model:file-list="fileList" - :action="updateUrl" + :action="uploadUrl" :auto-upload="autoUpload" :before-upload="beforeUpload" :drag="drag" - :headers="uploadHeaders" :limit="props.limit" :multiple="props.limit > 1" :on-error="excelUploadError" @@ -16,6 +15,7 @@ :on-remove="handleRemove" :on-success="handleFileSuccess" :show-file-list="true" + :http-request="httpRequest" class="upload-file-uploader" name="file" > @@ -36,9 +36,10 @@ </template> <script lang="ts" setup> import { propTypes } from '@/utils/propTypes' -import { getAccessToken, getTenantId } from '@/utils/auth' import type { UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus' 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' }) @@ -48,7 +49,6 @@ const emit = defineEmits(['update:modelValue']) const props = defineProps({ modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired, 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'] fileSize: propTypes.number.def(5), // 大小限制(MB) limit: propTypes.number.def(5), // 数量限制 @@ -62,10 +62,8 @@ const uploadRef = ref<UploadInstance>() const uploadList = ref<UploadUserFile[]>([]) const fileList = ref<UploadUserFile[]>([]) const uploadNumber = ref<number>(0) -const uploadHeaders = ref({ - Authorization: 'Bearer ' + getAccessToken(), - 'tenant-id': getTenantId() -}) + +const { uploadUrl, httpRequest } = useUpload() // 文件上传之前判断 const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => { @@ -120,10 +118,10 @@ const excelUploadError: UploadProps['onError'] = (): void => { message.error('导入数据失败,请您重新上传!') } // 删除上传文件 -const handleRemove = (file) => { - const findex = fileList.value.map((f) => f.name).indexOf(file.name) - if (findex > -1) { - fileList.value.splice(findex, 1) +const handleRemove = (file: UploadFile) => { + const index = fileList.value.map((f) => f.name).indexOf(file.name) + if (index > -1) { + fileList.value.splice(index, 1) emitUpdateModelValue() } } diff --git a/src/components/UploadFile/src/UploadImg.vue b/src/components/UploadFile/src/UploadImg.vue index a79c84b5..34566dda 100644 --- a/src/components/UploadFile/src/UploadImg.vue +++ b/src/components/UploadFile/src/UploadImg.vue @@ -3,15 +3,15 @@ <el-upload :id="uuid" :accept="fileType.join(',')" - :action="updateUrl" + :action="uploadUrl" :before-upload="beforeUpload" :class="['upload', drag ? 'no-border' : '']" :drag="drag" - :headers="uploadHeaders" :multiple="false" :on-error="uploadError" :on-success="uploadSuccess" :show-file-list="false" + :http-request="httpRequest" > <template v-if="modelValue"> <img :src="modelValue" class="upload-image" /> @@ -50,8 +50,8 @@ import type { UploadProps } from 'element-plus' import { generateUUID } from '@/utils' import { propTypes } from '@/utils/propTypes' -import { getAccessToken, getTenantId } from '@/utils/auth' import { createImageViewer } from '@/components/ImageViewer' +import { useUpload } from '@/components/UploadFile/src/useUpload' defineOptions({ name: 'UploadImg' }) @@ -70,7 +70,6 @@ type FileTypes = // 接受父组件参数 const props = defineProps({ modelValue: propTypes.string.def(''), - updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL), drag: propTypes.bool.def(true), // 是否支持拖拽上传 ==> 非必传(默认为 true) disabled: propTypes.bool.def(false), // 是否禁用上传组件 ==> 非必传(默认为 false) fileSize: propTypes.number.def(5), // 图片大小限制 ==> 非必传(默认为 5M) @@ -101,10 +100,7 @@ const deleteImg = () => { emit('update:modelValue', '') } -const uploadHeaders = ref({ - Authorization: 'Bearer ' + getAccessToken(), - 'tenant-id': getTenantId() -}) +const { uploadUrl, httpRequest } = useUpload() const editImg = () => { const dom = document.querySelector(`#${uuid.value} .el-upload__input`) diff --git a/src/components/UploadFile/src/UploadImgs.vue b/src/components/UploadFile/src/UploadImgs.vue index aa465b56..bd19ebbe 100644 --- a/src/components/UploadFile/src/UploadImgs.vue +++ b/src/components/UploadFile/src/UploadImgs.vue @@ -3,16 +3,16 @@ <el-upload v-model:file-list="fileList" :accept="fileType.join(',')" - :action="updateUrl" + :action="uploadUrl" :before-upload="beforeUpload" :class="['upload', drag ? 'no-border' : '']" :drag="drag" - :headers="uploadHeaders" :limit="limit" :multiple="true" :on-error="uploadError" :on-exceed="handleExceed" :on-success="uploadSuccess" + :http-request="httpRequest" list-type="picture-card" > <div class="upload-empty"> @@ -50,7 +50,7 @@ import type { UploadFile, UploadProps, UploadUserFile } from 'element-plus' import { ElNotification } from 'element-plus' import { propTypes } from '@/utils/propTypes' -import { getAccessToken, getTenantId } from '@/utils/auth' +import { useUpload } from '@/components/UploadFile/src/useUpload' defineOptions({ name: 'UploadImgs' }) @@ -70,7 +70,6 @@ type FileTypes = const props = defineProps({ 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) disabled: propTypes.bool.def(false), // 是否禁用上传组件 ==> 非必传(默认为 false) limit: propTypes.number.def(5), // 最大图片上传数 ==> 非必传(默认为 5张) @@ -81,10 +80,7 @@ const props = defineProps({ borderradius: propTypes.string.def('8px') // 组件边框圆角 ==> 非必传(默认为 8px) }) -const uploadHeaders = ref({ - Authorization: 'Bearer ' + getAccessToken(), - 'tenant-id': getTenantId() -}) +const { uploadUrl, httpRequest } = useUpload() const fileList = ref<UploadUserFile[]>([]) const uploadNumber = ref<number>(0) @@ -121,7 +117,6 @@ const emit = defineEmits<UploadEmits>() const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => { message.success('上传成功') // 删除自身 - debugger const index = fileList.value.findIndex((item) => item.response?.data === res.data) fileList.value.splice(index, 1) uploadList.value.push({ name: res.data, url: res.data }) diff --git a/src/components/UploadFile/src/useUpload.ts b/src/components/UploadFile/src/useUpload.ts new file mode 100644 index 00000000..bee0d584 --- /dev/null +++ b/src/components/UploadFile/src/useUpload.ts @@ -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' +} diff --git a/src/views/infra/file/FileForm.vue b/src/views/infra/file/FileForm.vue index beeaea01..1de1e253 100644 --- a/src/views/infra/file/FileForm.vue +++ b/src/views/infra/file/FileForm.vue @@ -3,16 +3,16 @@ <el-upload ref="uploadRef" v-model:file-list="fileList" - :action="url" + :action="uploadUrl" :auto-upload="false" :data="data" :disabled="formLoading" - :headers="uploadHeaders" :limit="1" :on-change="handleFileChange" :on-error="submitFormError" :on-exceed="handleExceed" :on-success="submitFormSuccess" + :http-request="httpRequest" accept=".jpg, .png, .gif" drag > @@ -31,7 +31,7 @@ </Dialog> </template> <script lang="ts" setup> -import { getAccessToken, getTenantId } from '@/utils/auth' +import { useUpload } from '@/components/UploadFile/src/useUpload' defineOptions({ name: 'InfraFileForm' }) @@ -40,12 +40,12 @@ const message = useMessage() // 消息弹窗 const dialogVisible = ref(false) // 弹窗的是否展示 const formLoading = ref(false) // 表单的加载中 -const url = import.meta.env.VITE_UPLOAD_URL -const uploadHeaders = ref() // 上传 Header 头 const fileList = ref([]) // 文件列表 const data = ref({ path: '' }) const uploadRef = ref() +const { uploadUrl, httpRequest } = useUpload() + /** 打开弹窗 */ const open = async () => { dialogVisible.value = true @@ -64,11 +64,6 @@ const submitFileForm = () => { message.error('请上传文件') return } - // 提交请求 - uploadHeaders.value = { - Authorization: 'Bearer ' + getAccessToken(), - 'tenant-id': getTenantId() - } unref(uploadRef)?.submit() } diff --git a/src/views/infra/file/index.vue b/src/views/infra/file/index.vue index 7732bb65..17967312 100644 --- a/src/views/infra/file/index.vue +++ b/src/views/infra/file/index.vue @@ -70,8 +70,17 @@ preview-teleported fit="cover" /> - <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> + <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> </el-table-column> <el-table-column