feat: 增加 image 文件上传(前端直传) 100%
parent
1bacb6759f
commit
87c6074e19
|
@ -7,6 +7,8 @@ VITE_BASE=/
|
||||||
VITE_BASE_URL='http://127.0.0.1:48080'
|
VITE_BASE_URL='http://127.0.0.1:48080'
|
||||||
# 接口地址
|
# 接口地址
|
||||||
VITE_GLOB_API_URL=/admin-api
|
VITE_GLOB_API_URL=/admin-api
|
||||||
|
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||||
|
VITE_UPLOAD_TYPE=server
|
||||||
# 是否打开 devtools,true 为打开,false 为关闭
|
# 是否打开 devtools,true 为打开,false 为关闭
|
||||||
VITE_DEVTOOLS=false
|
VITE_DEVTOOLS=false
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ VITE_BASE=/
|
||||||
VITE_BASE_URL='http://127.0.0.1:48080'
|
VITE_BASE_URL='http://127.0.0.1:48080'
|
||||||
# 接口地址
|
# 接口地址
|
||||||
VITE_GLOB_API_URL=/admin-api
|
VITE_GLOB_API_URL=/admin-api
|
||||||
|
# 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
|
||||||
|
VITE_UPLOAD_TYPE=server
|
||||||
|
|
||||||
# 是否开启压缩,可以设置为 none, brotli, gzip
|
# 是否开启压缩,可以设置为 none, brotli, gzip
|
||||||
VITE_COMPRESS=none
|
VITE_COMPRESS=none
|
||||||
|
|
|
@ -24,6 +24,12 @@ export namespace InfraFileApi {
|
||||||
uploadUrl: string; // 文件上传 URL
|
uploadUrl: string; // 文件上传 URL
|
||||||
url: 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);
|
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 });
|
return requestClient.upload('/infra/file/upload', data, { onUploadProgress });
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,7 @@ import { ref, toRefs, watch } from 'vue';
|
||||||
import { isFunction, isObject, isString } from '@vben/utils';
|
import { isFunction, isObject, isString } from '@vben/utils';
|
||||||
import { checkFileType } from './helper';
|
import { checkFileType } from './helper';
|
||||||
import { UploadResultStatus } from './typing';
|
import { UploadResultStatus } from './typing';
|
||||||
import { useUploadType } from './use-upload';
|
import { useUpload, useUploadType } from './use-upload';
|
||||||
import { uploadFile } from '#/api/infra/file';
|
|
||||||
|
|
||||||
defineOptions({ name: 'FileUpload', inheritAttrs: false });
|
defineOptions({ name: 'FileUpload', inheritAttrs: false });
|
||||||
|
|
||||||
|
@ -22,7 +21,7 @@ const props = withDefaults(
|
||||||
// 根据后缀,或者其他
|
// 根据后缀,或者其他
|
||||||
accept?: string[];
|
accept?: string[];
|
||||||
api?: (
|
api?: (
|
||||||
file: Blob | File,
|
file: File,
|
||||||
onUploadProgress?: AxiosProgressEvent,
|
onUploadProgress?: AxiosProgressEvent,
|
||||||
) => Promise<AxiosResponse<any>>;
|
) => Promise<AxiosResponse<any>>;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
@ -47,10 +46,7 @@ const props = withDefaults(
|
||||||
maxNumber: 1,
|
maxNumber: 1,
|
||||||
accept: () => [],
|
accept: () => [],
|
||||||
multiple: false,
|
multiple: false,
|
||||||
api: (file: Blob | File, onUploadProgress?: AxiosProgressEvent) => {
|
api: useUpload().httpRequest,
|
||||||
// TODO @芋艿:处理上传;前端上传
|
|
||||||
return uploadFile({ file }, onUploadProgress);
|
|
||||||
},
|
|
||||||
resultField: '',
|
resultField: '',
|
||||||
showDescription: false,
|
showDescription: false,
|
||||||
},
|
},
|
||||||
|
@ -171,7 +167,6 @@ function getValue() {
|
||||||
const list = (fileList.value || [])
|
const list = (fileList.value || [])
|
||||||
.filter((item) => item?.status === UploadResultStatus.DONE)
|
.filter((item) => item?.status === UploadResultStatus.DONE)
|
||||||
.map((item: any) => {
|
.map((item: any) => {
|
||||||
debugger
|
|
||||||
if (item?.response && props?.resultField) {
|
if (item?.response && props?.resultField) {
|
||||||
return item?.response;
|
return item?.response;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,7 @@ import { ref, toRefs, watch } from 'vue';
|
||||||
import { isFunction, isObject, isString } from '@vben/utils';
|
import { isFunction, isObject, isString } from '@vben/utils';
|
||||||
import { checkImgType, defaultImageAccepts } from './helper';
|
import { checkImgType, defaultImageAccepts } from './helper';
|
||||||
import { UploadResultStatus } from './typing';
|
import { UploadResultStatus } from './typing';
|
||||||
import { useUploadType } from './use-upload';
|
import { useUpload, useUploadType } from './use-upload';
|
||||||
import { uploadFile } from '#/api/infra/file';
|
|
||||||
|
|
||||||
defineOptions({ name: 'ImageUpload', inheritAttrs: false });
|
defineOptions({ name: 'ImageUpload', inheritAttrs: false });
|
||||||
|
|
||||||
|
@ -22,7 +21,7 @@ const props = withDefaults(
|
||||||
// 根据后缀,或者其他
|
// 根据后缀,或者其他
|
||||||
accept?: string[];
|
accept?: string[];
|
||||||
api?: (
|
api?: (
|
||||||
file: Blob | File,
|
file: File,
|
||||||
onUploadProgress?: AxiosProgressEvent,
|
onUploadProgress?: AxiosProgressEvent,
|
||||||
) => Promise<AxiosResponse<any>>;
|
) => Promise<AxiosResponse<any>>;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
@ -49,11 +48,7 @@ const props = withDefaults(
|
||||||
maxNumber: 1,
|
maxNumber: 1,
|
||||||
accept: () => defaultImageAccepts,
|
accept: () => defaultImageAccepts,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
api: (file: Blob | File, onUploadProgress?: AxiosProgressEvent) => {
|
api: useUpload().httpRequest,
|
||||||
// TODO @芋艿:处理上传;前端上传
|
|
||||||
debugger
|
|
||||||
return uploadFile({ file }, onUploadProgress);
|
|
||||||
},
|
|
||||||
resultField: '',
|
resultField: '',
|
||||||
showDescription: true,
|
showDescription: true,
|
||||||
},
|
},
|
||||||
|
@ -207,7 +202,6 @@ function getValue() {
|
||||||
const list = (fileList.value || [])
|
const list = (fileList.value || [])
|
||||||
.filter((item) => item?.status === UploadResultStatus.DONE)
|
.filter((item) => item?.status === UploadResultStatus.DONE)
|
||||||
.map((item: any) => {
|
.map((item: any) => {
|
||||||
debugger
|
|
||||||
if (item?.response && props?.resultField) {
|
if (item?.response && props?.resultField) {
|
||||||
return item?.response;
|
return item?.response;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
|
import type { AxiosProgressEvent, InfraFileApi } from '#/api/infra/file';
|
||||||
|
|
||||||
import { computed, unref } from 'vue';
|
import { computed, unref } from 'vue';
|
||||||
|
|
||||||
import { $t } from '@vben/locales';
|
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({
|
export function useUploadType({
|
||||||
acceptRef,
|
acceptRef,
|
||||||
|
@ -59,3 +62,95 @@ export function useUploadType({
|
||||||
});
|
});
|
||||||
return { getAccept, getStringAccept, getHelpText };
|
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'
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Upload } from 'ant-design-vue';
|
||||||
|
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { useVbenForm } from '#/adapter/form';
|
import { useVbenForm } from '#/adapter/form';
|
||||||
import { uploadFile } from '#/api/infra/file';
|
import { useUpload } from '#/components/upload/use-upload';
|
||||||
|
|
||||||
import { useFormSchema } from '../data';
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
// 提交表单
|
// 提交表单
|
||||||
const data = await formApi.getValues();
|
const data = await formApi.getValues();
|
||||||
try {
|
try {
|
||||||
await uploadFile(data);
|
await useUpload().httpRequest(data.file);
|
||||||
// 关闭并提示
|
// 关闭并提示
|
||||||
await modalApi.close();
|
await modalApi.close();
|
||||||
emit('success');
|
emit('success');
|
||||||
|
|
Loading…
Reference in New Issue