From 87c6074e192387221ceb1370c1e2ecdecfa83244 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 18 Apr 2025 18:30:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20image=20=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E4=B8=8A=E4=BC=A0=EF=BC=88=E5=89=8D=E7=AB=AF=E7=9B=B4?= =?UTF-8?q?=E4=BC=A0=EF=BC=89=20100%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-antd/.env.development | 2 + apps/web-antd/.env.production | 2 + apps/web-antd/src/api/infra/file/index.ts | 9 +- .../src/components/upload/file-upload.vue | 11 +-- .../src/components/upload/image-upload.vue | 12 +-- .../src/components/upload/use-upload.ts | 97 ++++++++++++++++++- .../src/views/infra/file/modules/form.vue | 4 +- 7 files changed, 115 insertions(+), 22 deletions(-) diff --git a/apps/web-antd/.env.development b/apps/web-antd/.env.development index ca6a2c250..4b4e441c9 100644 --- a/apps/web-antd/.env.development +++ b/apps/web-antd/.env.development @@ -7,6 +7,8 @@ VITE_BASE=/ VITE_BASE_URL='http://127.0.0.1:48080' # 接口地址 VITE_GLOB_API_URL=/admin-api +# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 +VITE_UPLOAD_TYPE=server # 是否打开 devtools,true 为打开,false 为关闭 VITE_DEVTOOLS=false diff --git a/apps/web-antd/.env.production b/apps/web-antd/.env.production index 177581ea0..ab51fee83 100644 --- a/apps/web-antd/.env.production +++ b/apps/web-antd/.env.production @@ -4,6 +4,8 @@ VITE_BASE=/ VITE_BASE_URL='http://127.0.0.1:48080' # 接口地址 VITE_GLOB_API_URL=/admin-api +# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 +VITE_UPLOAD_TYPE=server # 是否开启压缩,可以设置为 none, brotli, gzip VITE_COMPRESS=none diff --git a/apps/web-antd/src/api/infra/file/index.ts b/apps/web-antd/src/api/infra/file/index.ts index f9fad1640..15ce8fae3 100644 --- a/apps/web-antd/src/api/infra/file/index.ts +++ b/apps/web-antd/src/api/infra/file/index.ts @@ -24,6 +24,12 @@ export namespace InfraFileApi { uploadUrl: string; // 文件上传 URL url: string; // 文件 URL } + + /** 上传文件 */ + export interface FileUploadReqVO { + file: File; + path?: string; + } } /** 查询文件列表 */ @@ -50,8 +56,7 @@ export function createFile(data: InfraFileApi.InfraFile) { return requestClient.post('/infra/file/create', data); } -// TODO @芋艿:需要 data 自定义个类型; /** 上传文件 */ -export function uploadFile(data: any, onUploadProgress?: AxiosProgressEvent) { +export function uploadFile(data: InfraFileApi.FileUploadReqVO, onUploadProgress?: AxiosProgressEvent) { return requestClient.upload('/infra/file/upload', data, { onUploadProgress }); } diff --git a/apps/web-antd/src/components/upload/file-upload.vue b/apps/web-antd/src/components/upload/file-upload.vue index 04a75b873..c297d0aad 100644 --- a/apps/web-antd/src/components/upload/file-upload.vue +++ b/apps/web-antd/src/components/upload/file-upload.vue @@ -12,8 +12,7 @@ import { ref, toRefs, watch } from 'vue'; import { isFunction, isObject, isString } from '@vben/utils'; import { checkFileType } from './helper'; import { UploadResultStatus } from './typing'; -import { useUploadType } from './use-upload'; -import { uploadFile } from '#/api/infra/file'; +import { useUpload, useUploadType } from './use-upload'; defineOptions({ name: 'FileUpload', inheritAttrs: false }); @@ -22,7 +21,7 @@ const props = withDefaults( // 根据后缀,或者其他 accept?: string[]; api?: ( - file: Blob | File, + file: File, onUploadProgress?: AxiosProgressEvent, ) => Promise>; disabled?: boolean; @@ -47,10 +46,7 @@ const props = withDefaults( maxNumber: 1, accept: () => [], multiple: false, - api: (file: Blob | File, onUploadProgress?: AxiosProgressEvent) => { - // TODO @芋艿:处理上传;前端上传 - return uploadFile({ file }, onUploadProgress); - }, + api: useUpload().httpRequest, resultField: '', showDescription: false, }, @@ -171,7 +167,6 @@ function getValue() { const list = (fileList.value || []) .filter((item) => item?.status === UploadResultStatus.DONE) .map((item: any) => { - debugger if (item?.response && props?.resultField) { return item?.response; } diff --git a/apps/web-antd/src/components/upload/image-upload.vue b/apps/web-antd/src/components/upload/image-upload.vue index 691f2cc1f..e018f51d0 100644 --- a/apps/web-antd/src/components/upload/image-upload.vue +++ b/apps/web-antd/src/components/upload/image-upload.vue @@ -12,8 +12,7 @@ import { ref, toRefs, watch } from 'vue'; import { isFunction, isObject, isString } from '@vben/utils'; import { checkImgType, defaultImageAccepts } from './helper'; import { UploadResultStatus } from './typing'; -import { useUploadType } from './use-upload'; -import { uploadFile } from '#/api/infra/file'; +import { useUpload, useUploadType } from './use-upload'; defineOptions({ name: 'ImageUpload', inheritAttrs: false }); @@ -22,7 +21,7 @@ const props = withDefaults( // 根据后缀,或者其他 accept?: string[]; api?: ( - file: Blob | File, + file: File, onUploadProgress?: AxiosProgressEvent, ) => Promise>; disabled?: boolean; @@ -49,11 +48,7 @@ const props = withDefaults( maxNumber: 1, accept: () => defaultImageAccepts, multiple: false, - api: (file: Blob | File, onUploadProgress?: AxiosProgressEvent) => { - // TODO @芋艿:处理上传;前端上传 - debugger - return uploadFile({ file }, onUploadProgress); - }, + api: useUpload().httpRequest, resultField: '', showDescription: true, }, @@ -207,7 +202,6 @@ function getValue() { const list = (fileList.value || []) .filter((item) => item?.status === UploadResultStatus.DONE) .map((item: any) => { - debugger if (item?.response && props?.resultField) { return item?.response; } diff --git a/apps/web-antd/src/components/upload/use-upload.ts b/apps/web-antd/src/components/upload/use-upload.ts index 1aef937db..a11727c6e 100644 --- a/apps/web-antd/src/components/upload/use-upload.ts +++ b/apps/web-antd/src/components/upload/use-upload.ts @@ -1,8 +1,11 @@ import type { Ref } from 'vue'; +import type { AxiosProgressEvent, InfraFileApi } from '#/api/infra/file'; import { computed, unref } from 'vue'; - import { $t } from '@vben/locales'; +import CryptoJS from 'crypto-js' +import axios from 'axios' +import { uploadFile, getFilePresignedUrl, createFile } from '#/api/infra/file'; export function useUploadType({ acceptRef, @@ -59,3 +62,95 @@ export function useUploadType({ }); return { getAccept, getStringAccept, getHelpText }; } + +// TODO @芋艿:目前保持和 admin-vue3 一致,后续可能重构 +export const useUpload = () => { + // 后端上传地址 + const uploadUrl = getUploadUrl() + // 是否使用前端直连上传 + const isClientUpload = UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE + // 重写ElUpload上传方法 + const httpRequest = async (file: File, onUploadProgress?: AxiosProgressEvent) => { + // 模式一:前端上传 + if (isClientUpload) { + // 1.1 生成文件名称 + const fileName = await generateFileName(file) + // 1.2 获取文件预签名地址 + const presignedInfo = await getFilePresignedUrl(fileName) + // 1.3 上传文件 + return axios + .put(presignedInfo.uploadUrl, file, { + headers: { + 'Content-Type': file.type + } + }) + .then(() => { + // 1.4. 记录文件信息到后端(异步) + createFile0(presignedInfo, fileName, file) + // 通知成功,数据格式保持与后端上传的返回结果一致 + return { data: presignedInfo.url } + }) + } else { + // 模式二:后端上传 + return uploadFile({ file }, onUploadProgress); + } + } + + return { + uploadUrl, + httpRequest + } +} + +/** + * 获得上传 URL + */ +export const getUploadUrl = (): string => { + return import.meta.env.VITE_BASE_URL + import.meta.env.VITE_GLOB_API_URL + '/infra/file/upload' +} + +/** + * 创建文件信息 + * + * @param vo 文件预签名信息 + * @param name 文件名称 + * @param file 文件 + */ +function createFile0(vo: InfraFileApi.FilePresignedUrlRespVO, name: string, file: File) { + const fileVO = { + configId: vo.configId, + url: vo.url, + path: name, + name: file.name, + type: file.type, + size: file.size + } + createFile(fileVO) + return fileVO +} + +/** + * 生成文件名称(使用算法SHA256) + * + * @param file 要上传的文件 + */ +async function generateFileName(file: File) { + // 读取文件内容 + 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/apps/web-antd/src/views/infra/file/modules/form.vue b/apps/web-antd/src/views/infra/file/modules/form.vue index 10dc58857..9c3df1848 100644 --- a/apps/web-antd/src/views/infra/file/modules/form.vue +++ b/apps/web-antd/src/views/infra/file/modules/form.vue @@ -7,7 +7,7 @@ import { Upload } from 'ant-design-vue'; import { $t } from '#/locales'; import { useVbenForm } from '#/adapter/form'; -import { uploadFile } from '#/api/infra/file'; +import { useUpload } from '#/components/upload/use-upload'; import { useFormSchema } from '../data'; @@ -32,7 +32,7 @@ const [Modal, modalApi] = useVbenModal({ // 提交表单 const data = await formApi.getValues(); try { - await uploadFile(data); + await useUpload().httpRequest(data.file); // 关闭并提示 await modalApi.close(); emit('success');