upload+accessKey

This commit is contained in:
ryan
2026-06-07 21:21:06 +08:00
parent 360a26f109
commit 70a13dc107
35 changed files with 4763 additions and 1321 deletions
+1
View File
@@ -104,6 +104,7 @@ export type {
// 用户服务
export { UserService } from './user';
export type { AccessToken, CreateTokenResponse } from './user';
// 上传服务
export { UploadService } from './upload';
+57 -2
View File
@@ -1,7 +1,62 @@
/**
* 上传图片响应
* 文件上传元数据
*/
export interface UploadMetadata {
width?: number
height?: number
duration?: number
original_mime?: string
user_agent?: string
client_ip?: string
bucket?: string
extra?: Record<string, unknown>
}
/**
* 上传记录
*/
export interface Upload {
id: string
user_id: string
file_name: string
file_path: string
file_size: number
mime_type: string
extension: string
hash: string
storage_driver: string
type: string
status: string
metadata: UploadMetadata
created_at: string
updated_at: string
}
/**
* 上传接口响应(原 UploadImageResponse 兼容)
*/
export interface UploadImageResponse {
/** 上传记录 ID */
id: string;
id: string
}
/**
* 文件列表查询参数
*/
export interface ListUploadsQuery {
page?: number
page_size?: number
type?: string
extension?: string
keyword?: string
}
/**
* 文件列表分页响应
*/
export interface ListUploadsResponse {
total: number
page: number
page_size: number
items: Upload[]
}
+89 -59
View File
@@ -1,6 +1,6 @@
import { BaseService } from '../core/base.service';
import type { UploadImageResponse } from './types';
import type { InternalAxiosRequestConfig } from 'axios';
import {BaseService} from '../core/base.service'
import type {ListUploadsResponse, Upload, UploadImageResponse} from './types'
import type {InternalAxiosRequestConfig} from 'axios'
/**
* 根据上传ID构造文件访问URL
@@ -8,8 +8,19 @@ import type { InternalAxiosRequestConfig } from 'axios';
* @returns 文件访问URL
*/
export function getFileUrl(id: string | number | null | undefined): string | null {
if (!id) return null;
return `/f/${id}`;
if (!id) return null
return `/f/${id}`
}
/**
* 格式化文件大小
*/
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
}
/**
@@ -17,72 +28,91 @@ export function getFileUrl(id: string | number | null | undefined): string | nul
* 处理文件上传相关的 API 请求
*/
export class UploadService extends BaseService {
protected static readonly basePath = '/api/v1/upload';
protected static readonly basePath = '/api/v1/upload'
/**
* 上传红包封面图片
* @param file - 图片文件
* @param type - 封面类型 (cover: 背景封面, heterotypic: 异形装饰)
* @returns 上传后的图片URL
* @throws {ValidationError} 当文件格式或大小不符合要求时
* @throws {UnauthorizedError} 当未登录时
*
* @example
* ```typescript
* const file = e.target.files[0];
* const result = await UploadService.uploadRedEnvelopeCover(file, 'cover');
* console.log('图片URL:', result.url);
* ```
* 通用文件上传
* @param file - 文件对象
* @param type - 业务分类(如 avatar、attachment、generic)
* @param metadata - 可选额外 JSON 元数据
*/
static async uploadRedEnvelopeCover(
static async uploadFile(
file: File,
type: 'cover' | 'heterotypic'
): Promise<UploadImageResponse> {
// 验证文件类型
const allowedTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
throw new Error('只支持 JPG、PNG、WEBP 格式的图片');
type: string = 'generic',
metadata?: Record<string, unknown>
): Promise<Upload> {
const formData = new FormData()
formData.append('file', file)
formData.append('type', type)
if (metadata) {
formData.append('metadata', JSON.stringify(metadata))
}
// 验证文件大小 (最大 2MB)
const maxSize = 2 * 1024 * 1024;
if (file.size > maxSize) {
throw new Error('图片大小不能超过 2MB');
}
// 创建 FormData
const formData = new FormData();
formData.append('file', file);
formData.append('type', type);
return this.post<UploadImageResponse>('/redenvelope/cover', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
} as InternalAxiosRequestConfig);
return this.post<Upload>('', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
} as InternalAxiosRequestConfig)
}
/**
* 将 base64 图片转换为 Blob 并上传
* @param base64 - base64 编码的图片
* @param type - 封面类型
* @param filename - 文件名
* @returns 上传后的图片URL
* 获取我的文件列表
* @param page - 页码(1-based)
* @param pageSize - 每页数量
* @param keyword - 搜索关键词(文件名模糊)
* @param type - 业务分类过滤
* @param extension - 扩展名过滤
*/
static async listMyFiles(
page = 1,
pageSize = 20,
keyword?: string,
type?: string,
extension?: string
): Promise<ListUploadsResponse> {
const params: Record<string, string | number> = { page, page_size: pageSize }
if (keyword) params.keyword = keyword
if (type) params.type = type
if (extension) params.extension = extension
return this.get<ListUploadsResponse>('/my', { params })
}
/**
* 删除文件
*/
static async deleteFile(id: string): Promise<void> {
return this.delete<void>(`/${id}`)
}
/**
* 获取单文件下载 URL(触发 attachment 下载)
*/
static getDownloadUrl(id: string): string {
return `/api/v1/upload/download/${id}`
}
/**
* 批量 ZIP 打包下载
* @param ids - 文件 ID 数组
*/
static async batchDownload(ids: string[]): Promise<Blob> {
const response = await this.post<Blob>('/download/batch', { ids }, {
responseType: 'blob',
} as InternalAxiosRequestConfig)
return response
}
/**
* 将 base64 图片转换为 Blob 并上传(兼容旧接口)
*/
static async uploadBase64Image(
base64: string,
type: 'cover' | 'heterotypic',
type: string = 'generic',
filename: string = 'image.png'
): Promise<UploadImageResponse> {
// 将 base64 转换为 Blob
const response = await fetch(base64);
const blob = await response.blob();
// 创建 File 对象,确保正确的 MIME 类型
const mimeType = base64.match(/data:([^;]+);/)?.[1] || 'image/png';
const file = new File([blob], filename, { type: mimeType });
// 上传文件
return this.uploadRedEnvelopeCover(file, type);
const response = await fetch(base64)
const blob = await response.blob()
const mimeType = base64.match(/data:([^;]+);/)?.[1] || 'image/png'
const file = new File([blob], filename, { type: mimeType })
const result = await this.uploadFile(file, type)
return { id: result.id }
}
}
}
+1
View File
@@ -6,3 +6,4 @@
*/
export { UserService } from './user.service';
export type { AccessToken, CreateTokenResponse } from './user.service';
+47 -2
View File
@@ -1,4 +1,19 @@
import { BaseService } from '../core/base.service';
import {BaseService} from '../core/base.service';
export interface AccessToken {
id: number;
user_id: number;
name: string;
masked_token: string;
last_used_at?: string;
created_at: string;
updated_at: string;
}
export interface CreateTokenResponse {
token: string;
record: AccessToken;
}
/**
* 用户服务
@@ -6,5 +21,35 @@ import { BaseService } from '../core/base.service';
*/
export class UserService extends BaseService {
protected static readonly basePath = '/api/v1/user';
}
/**
* 获取当前用户的 AccessToken 列表
*/
static async getAccessTokens(): Promise<AccessToken[]> {
return this.get<AccessToken[]>('/access-tokens');
}
/**
* 创建一个新的 AccessToken
* @param name - 令牌名称
*/
static async createAccessToken(name: string): Promise<CreateTokenResponse> {
return this.post<CreateTokenResponse>('/access-tokens', { name });
}
/**
* 删除一个 AccessToken
* @param id - 令牌 ID
*/
static async deleteAccessToken(id: number): Promise<string> {
return this.delete<string>(`/access-tokens/${id}`);
}
/**
* 轮换一个 AccessToken 密钥
* @param id - 令牌 ID
*/
static async rotateAccessToken(id: number): Promise<CreateTokenResponse> {
return this.post<CreateTokenResponse>(`/access-tokens/${id}/rotate`);
}
}