mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-07 16:16:37 +08:00
upload+accessKey
This commit is contained in:
@@ -104,6 +104,7 @@ export type {
|
||||
|
||||
// 用户服务
|
||||
export { UserService } from './user';
|
||||
export type { AccessToken, CreateTokenResponse } from './user';
|
||||
|
||||
// 上传服务
|
||||
export { UploadService } from './upload';
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@
|
||||
*/
|
||||
|
||||
export { UserService } from './user.service';
|
||||
export type { AccessToken, CreateTokenResponse } from './user.service';
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user