fix: OSS客户端直传
This commit is contained in:
@@ -2,22 +2,67 @@ import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 文件上传
|
||||
* @param data
|
||||
* 通过后端服务器上传文件到OSS
|
||||
*
|
||||
* @param data 上传数据,包含file字段(文件对象)
|
||||
* @returns Promise 返回上传结果,包含url字段(文件URL)
|
||||
*/
|
||||
export async function uploadFile(data: any) {
|
||||
return requestClient.upload('upload/image', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件上传 - 聊天信息
|
||||
* @param data
|
||||
* 用于聊天场景的文件上传
|
||||
*
|
||||
* @param data 上传数据,包含file字段(文件对象)
|
||||
* @returns Promise 返回上传结果,包含url字段(文件URL)
|
||||
*/
|
||||
export async function uploadChatFile(data: any) {
|
||||
return requestClient.upload('upload/chat-file', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取OSS上传签名信息
|
||||
*
|
||||
* 该接口用于获取OSS直传所需的签名信息,包括:
|
||||
* - accessKeyId: 阿里云访问密钥ID
|
||||
* - policy: Base64编码的上传策略
|
||||
* - signature: 签名值
|
||||
* - host: OSS服务端点URL
|
||||
* - bucket: 存储桶名称(从后端环境变量获取,返回给前端)
|
||||
* - key: 文件路径前缀
|
||||
* - expire: 签名过期时间戳
|
||||
*
|
||||
* 使用场景:
|
||||
* - 前端需要直接上传文件到OSS时,先调用此接口获取签名
|
||||
* - 签名具有时效性,需要在有效期内使用
|
||||
* - 每次上传前建议重新获取签名,确保签名未过期
|
||||
*
|
||||
* 错误处理:
|
||||
* - 如果后端返回错误,会通过requestClient的统一错误处理机制处理
|
||||
* - 调用方需要捕获异常并进行相应处理
|
||||
*
|
||||
* @returns Promise<OssSignature> 返回OSS签名信息对象
|
||||
* @throws 如果API调用失败,会抛出错误
|
||||
*
|
||||
* @example
|
||||
* try {
|
||||
* const signature = await getOssSignature();
|
||||
* console.log('获取签名成功', signature);
|
||||
* } catch (error) {
|
||||
* console.error('获取签名失败', error);
|
||||
* }
|
||||
*/
|
||||
export async function getOssSignature() {
|
||||
return requestClient.get('/upload/oss-signature');
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过文件id集合获取文件信息
|
||||
* @param data
|
||||
*
|
||||
* @param data 请求数据,包含fileInfoIds字段(文件ID数组或逗号分隔的字符串)
|
||||
* @returns Promise 返回文件信息数组
|
||||
*/
|
||||
export async function getFileInfoByIds(data: any) {
|
||||
return requestClient.post('/sys/fileInfo/getFileInfoByIds', data);
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Upload } from 'ant-design-vue';
|
||||
|
||||
// 导入上传相关API和工具函数
|
||||
import { uploadFile } from '#/api/core/upload';
|
||||
import { uploadToOss } from '#/utils/oss-upload';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { Icon } from '#/components/icon';
|
||||
|
||||
defineOptions({
|
||||
@@ -19,12 +22,59 @@ const mValue = useVModel(props, 'value', emits, {
|
||||
defaultValue: props.value,
|
||||
passive: true,
|
||||
});
|
||||
const customRequest = (e: any) => {
|
||||
uploadFile({
|
||||
file: e.file,
|
||||
}).then((data: any) => {
|
||||
/**
|
||||
* 自定义上传请求处理函数
|
||||
*
|
||||
* 该函数根据preferences配置的上传方式,选择使用OSS直传或后端上传:
|
||||
* - 'direct': OSS直传模式,文件直接从浏览器上传到阿里云OSS
|
||||
* - 'backend': 后端上传模式,文件先上传到后端服务器,再由后端上传到OSS
|
||||
*
|
||||
* 上传方式说明:
|
||||
* 1. OSS直传(direct):
|
||||
* - 调用uploadToOss函数,直接上传到OSS
|
||||
* - 减少服务器负载,上传速度更快
|
||||
*
|
||||
* 2. 后端上传(backend):
|
||||
* - 调用uploadFile API,通过后端服务器上传
|
||||
* - 兼容现有功能,所有上传逻辑由后端统一处理
|
||||
*
|
||||
* 兼容性处理:
|
||||
* - 两种上传方式返回的数据结构保持一致:{ url: string }
|
||||
* - 上传成功后,将URL赋值给mValue,触发组件更新
|
||||
*
|
||||
* @param e 上传事件对象,包含file字段(文件对象)
|
||||
*/
|
||||
const customRequest = async (e: any) => {
|
||||
try {
|
||||
// 从preferences中读取上传方式配置
|
||||
// uploadMethod可能的值:'direct'(OSS直传)或 'backend'(后端上传)
|
||||
const uploadMethod = preferences.app.uploadMethod || 'direct';
|
||||
|
||||
let data: { url: string };
|
||||
|
||||
// 根据配置选择上传方式
|
||||
if (uploadMethod === 'direct') {
|
||||
// OSS直传模式:文件直接从浏览器上传到OSS
|
||||
// uploadToOss函数会处理签名获取、文件上传等所有逻辑
|
||||
data = await uploadToOss({
|
||||
file: e.file as File,
|
||||
});
|
||||
} else {
|
||||
// 后端上传模式:文件先上传到后端服务器,再由后端上传到OSS
|
||||
// uploadFile函数会将文件发送到后端API(/upload/image)
|
||||
data = await uploadFile({
|
||||
file: e.file,
|
||||
});
|
||||
}
|
||||
|
||||
// 上传成功,将URL赋值给mValue
|
||||
// mValue是双向绑定的值,更新后会触发父组件的更新
|
||||
mValue.value = data.url;
|
||||
});
|
||||
} catch (error) {
|
||||
// 上传失败,记录错误信息
|
||||
// 注意:这里没有显示错误提示,如果需要可以添加message.error
|
||||
console.error('头像上传失败', error);
|
||||
}
|
||||
};
|
||||
const handleRemove = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -7,7 +7,10 @@ import '@vueup/vue-quill/dist/vue-quill.snow.css';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
// 导入上传相关API和工具函数
|
||||
import { uploadFile } from '#/api/core/upload';
|
||||
import { uploadToOss } from '#/utils/oss-upload';
|
||||
import { preferences } from '@vben/preferences';
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
@@ -136,33 +139,82 @@ const insertImageToEditor = (quill: any, imageUrl: string): void => {
|
||||
};
|
||||
/**
|
||||
* 图片上传函数
|
||||
*
|
||||
* 该函数根据preferences配置的上传方式,选择使用OSS直传或后端上传:
|
||||
* - 'direct': OSS直传模式,文件直接从浏览器上传到阿里云OSS
|
||||
* - 'backend': 后端上传模式,文件先上传到后端服务器,再由后端上传到OSS
|
||||
*
|
||||
* 上传方式说明:
|
||||
* 1. OSS直传(direct):
|
||||
* - 调用uploadToOss函数,直接上传到OSS
|
||||
* - 减少服务器负载,上传速度更快
|
||||
* - 支持上传进度回调(如果需要可以添加)
|
||||
*
|
||||
* 2. 后端上传(backend):
|
||||
* - 调用uploadFile API,通过后端服务器上传
|
||||
* - 兼容现有功能,所有上传逻辑由后端统一处理
|
||||
*
|
||||
* 兼容性处理:
|
||||
* - 两种上传方式返回的数据结构保持一致:{ url: string }
|
||||
* - 上传成功或失败都会显示相应的提示信息
|
||||
* - 上传完成后,将isUpload标记设置为false,允许下次上传
|
||||
*
|
||||
* @param {File} file - 要上传的图片文件
|
||||
* @returns {Promise<string>} - 返回上传后的图片URL
|
||||
* @returns {Promise<string>} - 返回上传后的图片URL,如果上传失败返回空字符串
|
||||
*/
|
||||
const uploadImage = async (file: File): Promise<string> => {
|
||||
try {
|
||||
// 创建FormData对象,用于发送文件数据
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// 显示上传中的提示
|
||||
message.loading({ content: '图片上传中...', key: 'imageUpload' });
|
||||
|
||||
return uploadFile({
|
||||
file,
|
||||
}).then((data: any) => {
|
||||
message.success({
|
||||
content: '图片上传成功',
|
||||
key: 'imageUpload',
|
||||
duration: 2,
|
||||
// 从preferences中读取上传方式配置
|
||||
// uploadMethod可能的值:'direct'(OSS直传)或 'backend'(后端上传)
|
||||
const uploadMethod = preferences.app.uploadMethod || 'direct';
|
||||
|
||||
let data: { url: string };
|
||||
|
||||
// 根据配置选择上传方式
|
||||
if (uploadMethod === 'direct') {
|
||||
// OSS直传模式:文件直接从浏览器上传到OSS
|
||||
// uploadToOss函数会处理签名获取、文件上传等所有逻辑
|
||||
data = await uploadToOss({
|
||||
file: file,
|
||||
// 如果需要显示上传进度,可以添加onProgress回调
|
||||
// onProgress: (percent) => {
|
||||
// message.loading({ content: `图片上传中... ${percent}%`, key: 'imageUpload' });
|
||||
// },
|
||||
});
|
||||
isUpload.value = false;
|
||||
return data.url;
|
||||
} else {
|
||||
// 后端上传模式:文件先上传到后端服务器,再由后端上传到OSS
|
||||
// uploadFile函数会将文件发送到后端API(/upload/image)
|
||||
data = await uploadFile({
|
||||
file,
|
||||
});
|
||||
}
|
||||
|
||||
// 上传成功,显示成功提示
|
||||
message.success({
|
||||
content: '图片上传成功',
|
||||
key: 'imageUpload',
|
||||
duration: 2,
|
||||
});
|
||||
|
||||
// 重置上传标记,允许下次上传
|
||||
isUpload.value = false;
|
||||
|
||||
// 返回上传后的图片URL
|
||||
return data.url;
|
||||
} catch (error) {
|
||||
// 处理上传错误
|
||||
console.error('图片上传错误:', error);
|
||||
|
||||
// 显示错误提示
|
||||
message.error({ content: '图片上传失败', key: 'imageUpload', duration: 2 });
|
||||
|
||||
// 重置上传标记,允许重试
|
||||
isUpload.value = false;
|
||||
|
||||
// 返回空字符串,表示上传失败
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,7 +4,10 @@ import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { Modal, Upload } from 'ant-design-vue';
|
||||
import type { UploadFile } from 'ant-design-vue';
|
||||
|
||||
// 导入上传相关API和工具函数
|
||||
import { uploadFile } from '#/api/core/upload';
|
||||
import { uploadToOss } from '#/utils/oss-upload';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { Icon } from '#/components/icon';
|
||||
|
||||
defineOptions({
|
||||
@@ -132,11 +135,36 @@ const initSortable = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 使用原有的uploadFile接口(与upload-image.vue保持一致)
|
||||
/**
|
||||
* 自定义上传请求处理函数
|
||||
*
|
||||
* 该函数根据preferences配置的上传方式,选择使用OSS直传或后端上传:
|
||||
* - 'direct': OSS直传模式,文件直接从浏览器上传到阿里云OSS
|
||||
* - 'backend': 后端上传模式,文件先上传到后端服务器,再由后端上传到OSS
|
||||
*
|
||||
* 两种上传方式的区别:
|
||||
* 1. OSS直传(direct):
|
||||
* - 优点:减少服务器负载,上传速度更快,用户体验更好
|
||||
* - 缺点:需要后端提供签名接口,配置相对复杂
|
||||
* - 实现:调用uploadToOss函数,直接上传到OSS
|
||||
*
|
||||
* 2. 后端上传(backend):
|
||||
* - 优点:兼容现有功能,所有上传逻辑由后端统一处理
|
||||
* - 缺点:增加服务器负载,上传速度相对较慢
|
||||
* - 实现:调用uploadFile API,通过后端服务器上传
|
||||
*
|
||||
* 兼容性处理:
|
||||
* - 两种上传方式返回的数据结构保持一致:{ url: string }
|
||||
* - 确保无论使用哪种方式,组件的行为都是一致的
|
||||
* - 上传成功后,会重新初始化拖拽排序功能
|
||||
* - 如果上传失败,会更新文件状态为error,并调用onError回调
|
||||
*
|
||||
* @param options 上传选项,包含file、onProgress、onSuccess、onError等
|
||||
*/
|
||||
const customRequest = async (options: any) => {
|
||||
const { file, onProgress, onSuccess, onError } = options;
|
||||
|
||||
// 创建上传中的文件项
|
||||
// 创建上传中的文件项,用于在UI中显示上传状态
|
||||
const uploadingFile: FileItem = {
|
||||
uid: file.uid,
|
||||
name: file.name,
|
||||
@@ -144,15 +172,48 @@ const customRequest = async (options: any) => {
|
||||
url: '',
|
||||
};
|
||||
|
||||
// 将上传中的文件添加到文件列表
|
||||
fileList.value = [...fileList.value, uploadingFile];
|
||||
|
||||
try {
|
||||
// 使用原有的uploadFile接口
|
||||
const res = await uploadFile({
|
||||
file: file,
|
||||
});
|
||||
// 从preferences中读取上传方式配置
|
||||
// uploadMethod可能的值:'direct'(OSS直传)或 'backend'(后端上传)
|
||||
const uploadMethod = preferences.app.uploadMethod || 'direct';
|
||||
|
||||
// 更新文件状态为完成
|
||||
let res: { url: string };
|
||||
|
||||
// 根据配置选择上传方式
|
||||
if (uploadMethod === 'direct') {
|
||||
// OSS直传模式:文件直接从浏览器上传到OSS
|
||||
// uploadToOss函数会:
|
||||
// 1. 从后端获取OSS签名信息
|
||||
// 2. 生成全局唯一的文件名(UUID + 时间戳)
|
||||
// 3. 使用FormData构造POST请求直接上传到OSS
|
||||
// 4. 处理上传进度和错误
|
||||
// 5. 返回上传后的文件URL
|
||||
res = await uploadToOss({
|
||||
file: file as File,
|
||||
onProgress: (percent) => {
|
||||
// 将OSS上传进度传递给组件
|
||||
// percent范围:0-100,表示上传百分比
|
||||
if (onProgress) {
|
||||
onProgress(percent);
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 后端上传模式:文件先上传到后端服务器,再由后端上传到OSS
|
||||
// uploadFile函数会:
|
||||
// 1. 将文件发送到后端API(/upload/image)
|
||||
// 2. 后端接收文件后上传到OSS
|
||||
// 3. 返回上传后的文件URL
|
||||
res = await uploadFile({
|
||||
file: file,
|
||||
});
|
||||
}
|
||||
|
||||
// 上传成功,更新文件状态为完成
|
||||
// 将上传结果中的URL赋值给文件项
|
||||
const updatedFileList = fileList.value.map(item =>
|
||||
item.uid === file.uid
|
||||
? { ...item, status: 'done' as const, url: res.url }
|
||||
@@ -160,17 +221,24 @@ const customRequest = async (options: any) => {
|
||||
);
|
||||
|
||||
fileList.value = updatedFileList;
|
||||
|
||||
// 更新组件的modelValue,触发父组件的更新
|
||||
updateModelValue();
|
||||
|
||||
// 重新初始化排序
|
||||
// 重新初始化拖拽排序功能
|
||||
// 因为文件列表发生了变化,需要重新绑定拖拽事件
|
||||
nextTick(() => {
|
||||
initSortable();
|
||||
});
|
||||
|
||||
// 调用成功回调,通知上传组件上传已完成
|
||||
onSuccess(res);
|
||||
} catch (error) {
|
||||
// 上传失败,记录错误信息
|
||||
console.error('上传失败', error);
|
||||
|
||||
// 更新文件状态为错误
|
||||
// 错误状态的文件会在UI中显示错误图标
|
||||
const updatedFileList = fileList.value.map(item =>
|
||||
item.uid === file.uid
|
||||
? { ...item, status: 'error' as const }
|
||||
@@ -178,6 +246,8 @@ const customRequest = async (options: any) => {
|
||||
);
|
||||
|
||||
fileList.value = updatedFileList;
|
||||
|
||||
// 调用错误回调,通知上传组件上传失败
|
||||
onError(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,8 +3,10 @@ import { computed, ref, watch } from 'vue';
|
||||
import { Modal, Upload } from 'ant-design-vue';
|
||||
import type { UploadFile } from 'ant-design-vue';
|
||||
|
||||
// 恢复使用原有的上传接口
|
||||
// 导入上传相关API和工具函数
|
||||
import { uploadFile } from '#/api/core/upload';
|
||||
import { uploadToOss } from '#/utils/oss-upload';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { Icon } from '#/components/icon';
|
||||
|
||||
// 定义接口
|
||||
@@ -68,11 +70,35 @@ const updateModelValue = () => {
|
||||
emit('update:modelValue', urls);
|
||||
};
|
||||
|
||||
// 使用原有的uploadFile接口
|
||||
/**
|
||||
* 自定义上传请求处理函数
|
||||
*
|
||||
* 该函数根据preferences配置的上传方式,选择使用OSS直传或后端上传:
|
||||
* - 'direct': OSS直传模式,文件直接从浏览器上传到阿里云OSS
|
||||
* - 'backend': 后端上传模式,文件先上传到后端服务器,再由后端上传到OSS
|
||||
*
|
||||
* 两种上传方式的区别:
|
||||
* 1. OSS直传(direct):
|
||||
* - 优点:减少服务器负载,上传速度更快,用户体验更好
|
||||
* - 缺点:需要后端提供签名接口,配置相对复杂
|
||||
* - 实现:调用uploadToOss函数,直接上传到OSS
|
||||
*
|
||||
* 2. 后端上传(backend):
|
||||
* - 优点:兼容现有功能,所有上传逻辑由后端统一处理
|
||||
* - 缺点:增加服务器负载,上传速度相对较慢
|
||||
* - 实现:调用uploadFile API,通过后端服务器上传
|
||||
*
|
||||
* 兼容性处理:
|
||||
* - 两种上传方式返回的数据结构保持一致:{ url: string }
|
||||
* - 确保无论使用哪种方式,组件的行为都是一致的
|
||||
* - 如果上传失败,会更新文件状态为error,并调用onError回调
|
||||
*
|
||||
* @param options 上传选项,包含file、onProgress、onSuccess、onError等
|
||||
*/
|
||||
const customRequest = async (options: any) => {
|
||||
const { file, onProgress, onSuccess, onError } = options;
|
||||
|
||||
// 创建上传中的文件项
|
||||
// 创建上传中的文件项,用于在UI中显示上传状态
|
||||
const uploadingFile: FileItem = {
|
||||
uid: file.uid,
|
||||
name: file.name,
|
||||
@@ -80,15 +106,48 @@ const customRequest = async (options: any) => {
|
||||
url: '',
|
||||
};
|
||||
|
||||
// 将上传中的文件添加到文件列表
|
||||
fileList.value = [...fileList.value, uploadingFile];
|
||||
|
||||
try {
|
||||
// 使用原有的uploadFile接口
|
||||
const res = await uploadFile({
|
||||
file: file,
|
||||
});
|
||||
// 从preferences中读取上传方式配置
|
||||
// uploadMethod可能的值:'direct'(OSS直传)或 'backend'(后端上传)
|
||||
const uploadMethod = preferences.app.uploadMethod || 'direct';
|
||||
|
||||
// 更新文件状态为完成
|
||||
let res: { url: string };
|
||||
|
||||
// 根据配置选择上传方式
|
||||
if (uploadMethod === 'direct') {
|
||||
// OSS直传模式:文件直接从浏览器上传到OSS
|
||||
// uploadToOss函数会:
|
||||
// 1. 从后端获取OSS签名信息
|
||||
// 2. 生成全局唯一的文件名(UUID + 时间戳)
|
||||
// 3. 使用FormData构造POST请求直接上传到OSS
|
||||
// 4. 处理上传进度和错误
|
||||
// 5. 返回上传后的文件URL
|
||||
res = await uploadToOss({
|
||||
file: file as File,
|
||||
onProgress: (percent) => {
|
||||
// 将OSS上传进度传递给组件
|
||||
// percent范围:0-100,表示上传百分比
|
||||
if (onProgress) {
|
||||
onProgress(percent);
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 后端上传模式:文件先上传到后端服务器,再由后端上传到OSS
|
||||
// uploadFile函数会:
|
||||
// 1. 将文件发送到后端API(/upload/image)
|
||||
// 2. 后端接收文件后上传到OSS
|
||||
// 3. 返回上传后的文件URL
|
||||
res = await uploadFile({
|
||||
file: file,
|
||||
});
|
||||
}
|
||||
|
||||
// 上传成功,更新文件状态为完成
|
||||
// 将上传结果中的URL赋值给文件项
|
||||
const updatedFileList = fileList.value.map(item =>
|
||||
item.uid === file.uid
|
||||
? { ...item, status: 'done' as const, url: res.url }
|
||||
@@ -96,11 +155,18 @@ const customRequest = async (options: any) => {
|
||||
);
|
||||
|
||||
fileList.value = updatedFileList;
|
||||
|
||||
// 更新组件的modelValue,触发父组件的更新
|
||||
updateModelValue();
|
||||
|
||||
// 调用成功回调,通知上传组件上传已完成
|
||||
onSuccess(res);
|
||||
} catch (error) {
|
||||
// 上传失败,记录错误信息
|
||||
console.error('上传失败', error);
|
||||
|
||||
// 更新文件状态为错误
|
||||
// 错误状态的文件会在UI中显示错误图标
|
||||
const updatedFileList = fileList.value.map(item =>
|
||||
item.uid === file.uid
|
||||
? { ...item, status: 'error' as const }
|
||||
@@ -108,6 +174,8 @@ const customRequest = async (options: any) => {
|
||||
);
|
||||
|
||||
fileList.value = updatedFileList;
|
||||
|
||||
// 调用错误回调,通知上传组件上传失败
|
||||
onError(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,6 +34,24 @@ export const overridesPreferences = defineOverridesPreferences({
|
||||
// 检查更新的时间间隔,单位为分钟
|
||||
checkUpdatesInterval: 1,
|
||||
version: '1.0.0',
|
||||
/**
|
||||
* 文件上传方式配置
|
||||
*
|
||||
* 可选值:
|
||||
* - 'direct': OSS直传模式,文件直接从浏览器上传到阿里云OSS(推荐,性能更好)
|
||||
* - 'backend': 后端上传模式,文件先上传到后端服务器,再由后端上传到OSS(兼容模式)
|
||||
*
|
||||
* 默认值:'direct'(OSS直传)
|
||||
*
|
||||
* 说明:
|
||||
* - OSS直传模式:减少服务器负载,上传速度更快,但需要后端提供签名接口
|
||||
* - 后端上传模式:兼容现有功能,所有上传逻辑由后端处理
|
||||
*
|
||||
* 切换方式:
|
||||
* - 可以通过修改此配置值来切换上传方式
|
||||
* - 配置会持久化到本地存储,刷新页面后仍然有效
|
||||
*/
|
||||
uploadMethod: 'direct' as 'direct' | 'backend',
|
||||
},
|
||||
sidebar: {
|
||||
extraCollapse: false,
|
||||
|
||||
345
apps/web-antd/src/utils/oss-upload.ts
Normal file
345
apps/web-antd/src/utils/oss-upload.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* OSS直传工具函数
|
||||
*
|
||||
* 该模块实现了阿里云OSS的Web直传功能,支持客户端直接上传文件到OSS存储桶
|
||||
* 参考文档:https://help.aliyun.com/zh/oss/user-guide/obtain-signature-information-from-the-server-and-upload-data-to-oss
|
||||
*
|
||||
* 主要功能:
|
||||
* 1. 从后端获取OSS上传签名信息
|
||||
* 2. 生成全局唯一的文件名(UUID + 时间戳)
|
||||
* 3. 使用FormData构造POST请求直接上传到OSS
|
||||
* 4. 处理上传进度和错误
|
||||
* 5. 返回上传后的文件URL
|
||||
*/
|
||||
|
||||
import { getOssSignature } from '#/api/core/upload';
|
||||
|
||||
/**
|
||||
* OSS签名信息接口
|
||||
* 后端返回的签名数据结构
|
||||
*/
|
||||
export interface OssSignature {
|
||||
/** 阿里云AccessKeyId */
|
||||
accessKeyId: string;
|
||||
/** Base64编码的PostPolicy策略 */
|
||||
policy: string;
|
||||
/** 签名值 */
|
||||
signature: string;
|
||||
/** OSS服务端点URL */
|
||||
host: string;
|
||||
/** 存储桶名称(从后端返回,不能硬编码) */
|
||||
bucket: string;
|
||||
/** 文件路径前缀(仅作为路径前缀,完整文件名由前端生成) */
|
||||
key: string;
|
||||
/** 签名过期时间戳(Unix时间戳,秒级) */
|
||||
expire: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传选项接口
|
||||
*/
|
||||
export interface UploadOptions {
|
||||
/** 要上传的文件对象 */
|
||||
file: File;
|
||||
/** 上传进度回调函数,参数为0-100的进度百分比 */
|
||||
onProgress?: (percent: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传结果接口
|
||||
*/
|
||||
export interface UploadResult {
|
||||
/** 上传后的文件完整URL */
|
||||
url: string;
|
||||
/** 文件在OSS中的objectName(相对路径) */
|
||||
objectName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成全局唯一的文件名
|
||||
*
|
||||
* 文件名格式:xk_upload_日期/UUID_时间戳.扩展名
|
||||
* 例如:xk_upload_20250108/550e8400-e29b-41d4-a716-446655440000_1736323200.jpg
|
||||
*
|
||||
* 规则说明:
|
||||
* - 基础路径:xk_upload_日期/ (日期格式:YYYYMMDD)
|
||||
* - 文件名:UUID_时间戳.扩展名
|
||||
* - UUID:使用浏览器原生crypto.randomUUID()生成,确保全局唯一
|
||||
* - 时间戳:使用Unix时间戳(秒级),通过Math.floor(Date.now() / 1000)获取
|
||||
* - 扩展名:从原始文件名提取
|
||||
*
|
||||
* @param file 文件对象,用于获取文件名和扩展名
|
||||
* @returns 返回完整的objectName路径
|
||||
*
|
||||
* @example
|
||||
* const objectName = generateObjectName(file);
|
||||
* // 返回: "xk_upload_20250108/550e8400-e29b-41d4-a716-446655440000_1736323200.jpg"
|
||||
*/
|
||||
export function generateObjectName(file: File): string {
|
||||
// 获取当前日期,格式:YYYYMMDD
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(today.getDate()).padStart(2, '0');
|
||||
const dateStr = `${year}${month}${day}`;
|
||||
|
||||
// 生成UUID,使用浏览器原生API确保全局唯一
|
||||
// crypto.randomUUID() 是Web Crypto API的一部分,支持现代浏览器
|
||||
let uuid: string;
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||
uuid = crypto.randomUUID();
|
||||
} else {
|
||||
// 降级方案:如果浏览器不支持randomUUID,使用替代方案
|
||||
// 生成类似UUID格式的随机字符串
|
||||
uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
// 获取Unix时间戳(秒级)
|
||||
// Date.now()返回毫秒级时间戳,除以1000并向下取整得到秒级时间戳
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
// 获取文件扩展名
|
||||
// 从文件名中提取扩展名,如果没有扩展名则使用空字符串
|
||||
const fileName = file.name;
|
||||
const lastDotIndex = fileName.lastIndexOf('.');
|
||||
const extension = lastDotIndex > 0 ? fileName.substring(lastDotIndex + 1).toLowerCase() : '';
|
||||
|
||||
// 组合成完整的objectName路径
|
||||
// 格式:xk_upload_日期/UUID_时间戳.扩展名
|
||||
const objectName = `xk_upload_${dateStr}/${uuid}_${timestamp}${extension ? '.' + extension : ''}`;
|
||||
|
||||
return objectName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从后端获取OSS上传签名信息
|
||||
*
|
||||
* 该函数调用后端API获取OSS上传所需的签名信息,包括:
|
||||
* - accessKeyId: 阿里云访问密钥ID
|
||||
* - policy: Base64编码的上传策略
|
||||
* - signature: 签名值
|
||||
* - host: OSS服务端点
|
||||
* - bucket: 存储桶名称(从后端返回,确保配置灵活性)
|
||||
* - key: 文件路径前缀
|
||||
* - expire: 签名过期时间
|
||||
*
|
||||
* 错误处理:
|
||||
* - 如果API调用失败,会抛出错误
|
||||
* - 调用方需要捕获错误并进行相应处理
|
||||
*
|
||||
* @returns Promise<OssSignature> 返回签名信息对象
|
||||
* @throws 如果API调用失败或返回数据格式不正确,会抛出错误
|
||||
*
|
||||
* @example
|
||||
* try {
|
||||
* const signature = await getOssSignature();
|
||||
* console.log('获取签名成功', signature);
|
||||
* } catch (error) {
|
||||
* console.error('获取签名失败', error);
|
||||
* }
|
||||
*/
|
||||
export async function getOssSignatureFromBackend(): Promise<OssSignature> {
|
||||
try {
|
||||
// 调用后端API获取签名信息
|
||||
const response = await getOssSignature();
|
||||
|
||||
// 验证返回数据的完整性
|
||||
if (!response || !response.accessKeyId || !response.policy || !response.signature) {
|
||||
throw new Error('后端返回的签名信息不完整');
|
||||
}
|
||||
|
||||
// 确保bucket字段存在(重要:bucket必须从后端返回)
|
||||
if (!response.bucket) {
|
||||
throw new Error('后端未返回bucket信息,无法进行上传');
|
||||
}
|
||||
|
||||
return response as OssSignature;
|
||||
} catch (error) {
|
||||
// 记录错误信息,便于调试
|
||||
console.error('获取OSS签名失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到OSS
|
||||
*
|
||||
* 该函数实现了完整的OSS直传流程:
|
||||
* 1. 从后端获取签名信息
|
||||
* 2. 生成全局唯一的文件名
|
||||
* 3. 构造FormData并设置必要的字段
|
||||
* 4. 使用XMLHttpRequest发送POST请求到OSS
|
||||
* 5. 处理上传进度和错误
|
||||
* 6. 返回上传后的文件URL
|
||||
*
|
||||
* 上传流程说明:
|
||||
* - 使用FormData构造multipart/form-data格式的请求
|
||||
* - 必须包含的字段:key, policy, OSSAccessKeyId, signature, file
|
||||
* - key字段使用生成的objectName
|
||||
* - 上传到OSS的host端点
|
||||
* - 上传成功后,文件URL格式:host/objectName
|
||||
*
|
||||
* 进度处理:
|
||||
* - 使用XMLHttpRequest的upload.onprogress事件监听上传进度
|
||||
* - 进度值范围:0-100,表示上传百分比
|
||||
* - 通过onProgress回调函数通知调用方
|
||||
*
|
||||
* 错误处理:
|
||||
* - 网络错误:捕获XMLHttpRequest的错误事件
|
||||
* - OSS错误:解析OSS返回的XML错误响应
|
||||
* - 签名过期:检查签名是否在有效期内
|
||||
*
|
||||
* @param options 上传选项,包含文件和进度回调
|
||||
* @returns Promise<UploadResult> 返回上传结果,包含文件URL和objectName
|
||||
* @throws 如果上传失败,会抛出包含错误信息的异常
|
||||
*
|
||||
* @example
|
||||
* try {
|
||||
* const result = await uploadToOss({
|
||||
* file: fileObject,
|
||||
* onProgress: (percent) => {
|
||||
* console.log(`上传进度: ${percent}%`);
|
||||
* }
|
||||
* });
|
||||
* console.log('上传成功,文件URL:', result.url);
|
||||
* } catch (error) {
|
||||
* console.error('上传失败', error);
|
||||
* }
|
||||
*/
|
||||
export async function uploadToOss(options: UploadOptions): Promise<UploadResult> {
|
||||
const { file, onProgress } = options;
|
||||
|
||||
// 步骤1:从后端获取OSS签名信息
|
||||
// 签名信息包含上传所需的所有认证参数
|
||||
const signature = await getOssSignatureFromBackend();
|
||||
|
||||
// 步骤2:生成全局唯一的文件名
|
||||
// 使用UUID + 时间戳确保文件名全局唯一,避免冲突
|
||||
const objectName = generateObjectName(file);
|
||||
|
||||
// 步骤3:构造FormData,设置OSS上传所需的字段
|
||||
// FormData用于构造multipart/form-data格式的请求体
|
||||
const formData = new FormData();
|
||||
|
||||
// key: 文件在OSS中的完整路径(objectName)
|
||||
// 这是OSS中文件的唯一标识
|
||||
formData.append('key', objectName);
|
||||
|
||||
// policy: Base64编码的上传策略
|
||||
// 策略定义了上传的条件,如文件大小限制、允许的文件类型等
|
||||
formData.append('policy', signature.policy);
|
||||
|
||||
// OSSAccessKeyId: 阿里云访问密钥ID
|
||||
// 用于标识上传请求的发起者
|
||||
formData.append('OSSAccessKeyId', signature.accessKeyId);
|
||||
|
||||
// signature: 签名值
|
||||
// 使用AccessKeySecret对policy进行签名,确保策略未被篡改
|
||||
formData.append('signature', signature.signature);
|
||||
|
||||
// success_action_status: 指定上传成功后OSS返回的HTTP状态码
|
||||
// 默认是204(无内容),设置为200后OSS返回200状态码
|
||||
// 这样前端可以通过200状态码明确判断上传成功
|
||||
// 注意:此字段必须与后端Policy中的条件一致,否则会上传失败
|
||||
formData.append('success_action_status', '200');
|
||||
|
||||
// x-oss-object-acl: 指定文件的访问权限(ACL)
|
||||
// 设置为 public-read 表示文件可以被公开访问,无需签名URL
|
||||
// 这与后端上传保持一致(后端上传后会调用putObjectAcl设置公共读)
|
||||
// 注意:此字段必须与后端Policy中的条件一致,否则会上传失败
|
||||
formData.append('x-oss-object-acl', 'public-read');
|
||||
|
||||
// file: 要上传的文件
|
||||
// 必须是最后一个字段,OSS要求file字段在FormData的最后
|
||||
formData.append('file', file);
|
||||
|
||||
// 步骤4:使用XMLHttpRequest发送POST请求到OSS
|
||||
// 使用XMLHttpRequest而不是fetch,因为需要监听上传进度
|
||||
return new Promise<UploadResult>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
// 监听上传进度
|
||||
// upload.onprogress事件在上传过程中会多次触发
|
||||
if (onProgress && xhr.upload) {
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
if (event.lengthComputable) {
|
||||
// 计算上传进度百分比
|
||||
// loaded: 已上传的字节数
|
||||
// total: 文件总字节数
|
||||
const percent = Math.round((event.loaded / event.total) * 100);
|
||||
onProgress(percent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 监听请求完成事件
|
||||
xhr.addEventListener('load', () => {
|
||||
// 检查HTTP状态码
|
||||
// 200-299表示成功,204表示成功但无内容
|
||||
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 204) {
|
||||
// 上传成功,构造文件URL
|
||||
// URL格式:host/objectName
|
||||
// 注意:使用后端返回的host,确保配置的灵活性
|
||||
const fileUrl = `${signature.host}/${objectName}`;
|
||||
|
||||
resolve({
|
||||
url: fileUrl,
|
||||
objectName: objectName,
|
||||
});
|
||||
} else {
|
||||
// HTTP状态码表示错误,尝试解析OSS返回的错误信息
|
||||
let errorMessage = `上传失败,HTTP状态码: ${xhr.status}`;
|
||||
|
||||
try {
|
||||
// OSS错误响应通常是XML格式
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(xhr.responseText, 'text/xml');
|
||||
const errorNode = xmlDoc.querySelector('Error');
|
||||
|
||||
if (errorNode) {
|
||||
const codeNode = errorNode.querySelector('Code');
|
||||
const messageNode = errorNode.querySelector('Message');
|
||||
|
||||
if (codeNode && messageNode) {
|
||||
errorMessage = `OSS错误 [${codeNode.textContent}]: ${messageNode.textContent}`;
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
// 如果解析失败,使用默认错误信息
|
||||
console.warn('解析OSS错误响应失败:', parseError);
|
||||
}
|
||||
|
||||
reject(new Error(errorMessage));
|
||||
}
|
||||
});
|
||||
|
||||
// 监听网络错误事件
|
||||
// 当网络请求失败时触发(如网络断开、超时等)
|
||||
xhr.addEventListener('error', () => {
|
||||
reject(new Error('网络错误:无法连接到OSS服务器'));
|
||||
});
|
||||
|
||||
// 监听请求超时事件
|
||||
// 如果请求时间超过timeout设置的时间,会触发此事件
|
||||
xhr.addEventListener('timeout', () => {
|
||||
reject(new Error('上传超时:请求时间过长'));
|
||||
});
|
||||
|
||||
// 设置请求超时时间(30秒)
|
||||
// 对于大文件,可能需要增加超时时间
|
||||
xhr.timeout = 30000;
|
||||
|
||||
// 打开POST请求
|
||||
// 使用后端返回的host作为请求URL
|
||||
xhr.open('POST', signature.host, true);
|
||||
|
||||
// 发送请求
|
||||
// 将FormData作为请求体发送
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user