163 lines
4.1 KiB
TypeScript
163 lines
4.1 KiB
TypeScript
|
|
import axios from "axios";
|
|||
|
|
import { buildOssDirPath } from "./env";
|
|||
|
|
|
|||
|
|
// ==================== 类型定义 ====================
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* OSS 直传 Token 响应
|
|||
|
|
*/
|
|||
|
|
export interface OssToken {
|
|||
|
|
/** OSS 访问 ID */
|
|||
|
|
accessid: string;
|
|||
|
|
/** Base64 编码的 Policy */
|
|||
|
|
policy: string;
|
|||
|
|
/** 签名 */
|
|||
|
|
signature: string;
|
|||
|
|
/** 上传目录前缀 */
|
|||
|
|
dir: string;
|
|||
|
|
/** OSS 上传地址(https://bucketname.endpoint) */
|
|||
|
|
host: string;
|
|||
|
|
/** 完整文件路径(dir + 日期路径 + UUID文件名) */
|
|||
|
|
key: string;
|
|||
|
|
/** 过期时间(秒) */
|
|||
|
|
expire: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 上传结果
|
|||
|
|
*/
|
|||
|
|
export interface UploadResult {
|
|||
|
|
success: boolean;
|
|||
|
|
filePath: string;
|
|||
|
|
fileName: string;
|
|||
|
|
fileSize: number;
|
|||
|
|
mimeType: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== 配置 ====================
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 后端 API 基础地址
|
|||
|
|
* 根据你的项目修改此值
|
|||
|
|
*/
|
|||
|
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "/api";
|
|||
|
|
|
|||
|
|
// ==================== 核心方法 ====================
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取阿里云 OSS 直传 Token
|
|||
|
|
*
|
|||
|
|
* @param fileName 文件名(如:图片.jpg)
|
|||
|
|
* @param dir 业务目录(如:avatar, course/cover),会自动添加环境前缀
|
|||
|
|
* @returns OSS 直传 Token
|
|||
|
|
*/
|
|||
|
|
export async function getOssToken(
|
|||
|
|
fileName: string,
|
|||
|
|
dir?: string,
|
|||
|
|
): Promise<OssToken> {
|
|||
|
|
// 自动添加环境前缀(dev/test/prod)
|
|||
|
|
const fullDir = buildOssDirPath(dir);
|
|||
|
|
|
|||
|
|
const response = await axios.get<{ code: number; data: OssToken }>(
|
|||
|
|
`${API_BASE_URL}/v1/files/oss/token`,
|
|||
|
|
{
|
|||
|
|
params: { fileName, dir: fullDir },
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
return response.data.data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 直接上传文件到阿里云 OSS
|
|||
|
|
*
|
|||
|
|
* 使用 FormData + POST 方式直传,无需经过后端中转。
|
|||
|
|
* 支持:进度回调、取消上传、超时设置。
|
|||
|
|
*
|
|||
|
|
* @param file 要上传的文件
|
|||
|
|
* @param token OSS Token(通过 getOssToken 获取)
|
|||
|
|
* @param options 可选配置
|
|||
|
|
* @returns 上传后的文件 URL
|
|||
|
|
*/
|
|||
|
|
export async function uploadToOss(
|
|||
|
|
file: File,
|
|||
|
|
token: OssToken,
|
|||
|
|
options?: {
|
|||
|
|
/** 上传进度回调(0-100) */
|
|||
|
|
onProgress?: (percent: number) => void;
|
|||
|
|
/** 取消信号 */
|
|||
|
|
signal?: AbortSignal;
|
|||
|
|
/** 超时时间(毫秒),默认 5 分钟 */
|
|||
|
|
timeout?: number;
|
|||
|
|
},
|
|||
|
|
): Promise<{ url: string }> {
|
|||
|
|
const opts = options ?? {};
|
|||
|
|
const formData = new FormData();
|
|||
|
|
|
|||
|
|
// 按照阿里云 OSS PostObject 要求构造表单
|
|||
|
|
// 注意:file 必须为最后一个表单域
|
|||
|
|
formData.append("success_action_status", "200"); // 成功时返回 200
|
|||
|
|
formData.append("OSSAccessKeyId", token.accessid);
|
|||
|
|
formData.append("policy", token.policy);
|
|||
|
|
formData.append("signature", token.signature);
|
|||
|
|
formData.append("key", token.key);
|
|||
|
|
formData.append("file", file); // file 必须为最后一个表单域
|
|||
|
|
|
|||
|
|
await axios.post(token.host, formData, {
|
|||
|
|
headers: {
|
|||
|
|
"Content-Type": "multipart/form-data",
|
|||
|
|
},
|
|||
|
|
timeout: opts.timeout ?? 1000 * 60 * 5, // 默认 5 分钟
|
|||
|
|
signal: opts.signal,
|
|||
|
|
onUploadProgress: (progressEvent) => {
|
|||
|
|
if (opts.onProgress) {
|
|||
|
|
const percent =
|
|||
|
|
progressEvent.progress != null
|
|||
|
|
? progressEvent.progress * 100
|
|||
|
|
: progressEvent.total
|
|||
|
|
? (progressEvent.loaded * 100) / progressEvent.total
|
|||
|
|
: 0;
|
|||
|
|
opts.onProgress(Math.round(percent));
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
url: `${token.host}/${token.key}`,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 一站式上传文件(获取 Token + 直传 OSS)
|
|||
|
|
*
|
|||
|
|
* @param file 要上传的文件
|
|||
|
|
* @param dir 业务目录(如:avatar, course/cover)
|
|||
|
|
* @param options 可选配置
|
|||
|
|
* @returns 上传结果
|
|||
|
|
*/
|
|||
|
|
export async function uploadFile(
|
|||
|
|
file: File,
|
|||
|
|
dir?: string,
|
|||
|
|
options?: {
|
|||
|
|
onProgress?: (percent: number) => void;
|
|||
|
|
signal?: AbortSignal;
|
|||
|
|
},
|
|||
|
|
): Promise<UploadResult> {
|
|||
|
|
// 1. 获取 OSS 直传 Token
|
|||
|
|
const token = await getOssToken(file.name, dir);
|
|||
|
|
|
|||
|
|
// 2. 直传到 OSS
|
|||
|
|
await uploadToOss(file, token, {
|
|||
|
|
onProgress: options?.onProgress,
|
|||
|
|
signal: options?.signal,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 3. 返回结果
|
|||
|
|
return {
|
|||
|
|
success: true,
|
|||
|
|
filePath: `${token.host}/${token.key}`,
|
|||
|
|
fileName: file.name,
|
|||
|
|
fileSize: file.size,
|
|||
|
|
mimeType: file.type,
|
|||
|
|
};
|
|||
|
|
}
|