mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 22:06:38 +08:00
5b13d5b464
- Expose user-scoped CRUD APIs under `/api/v1/upload` (my files query, stats, rename, delete) - Update backend handlers and routers with ownership validation checks - Create a dedicated frontend personal file manager card-list and upload button under `/files` - Add comprehensive backend test coverage and update API docs
190 lines
5.4 KiB
TypeScript
190 lines
5.4 KiB
TypeScript
import {BaseService} from '../core/base.service'
|
||
import type {ListUploadsResponse, Upload, UploadImageResponse, FileStatsResponse} from './types'
|
||
import type {InternalAxiosRequestConfig} from 'axios'
|
||
import apiClient from '../core/api-client'
|
||
|
||
export type ImageQuality = 'low' | 'medium' | 'high' | 'origin'
|
||
|
||
/**
|
||
* 根据上传ID构造文件访问URL
|
||
* @param id - 上传记录ID
|
||
* @param quality - 图片质量
|
||
* @returns 文件访问URL
|
||
*/
|
||
export function getFileUrl(
|
||
id: string | number | null | undefined,
|
||
quality: ImageQuality = 'origin'
|
||
): string | null {
|
||
if (!id) return null
|
||
if (quality === 'origin') return `/f/${id}`
|
||
return `/f/${id}?quality=${quality}`
|
||
}
|
||
|
||
/**
|
||
* 格式化文件大小
|
||
*/
|
||
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]}`
|
||
}
|
||
|
||
/**
|
||
* 上传服务
|
||
* 处理文件上传相关的 API 请求
|
||
*/
|
||
export class UploadService extends BaseService {
|
||
protected static readonly basePath = '/api/v1/admin/uploads'
|
||
|
||
/**
|
||
* 通用文件上传
|
||
* @param file - 文件对象
|
||
* @param type - 业务分类(如 avatar、attachment、generic)
|
||
* @param metadata - 可选额外 JSON 元数据
|
||
*/
|
||
static async uploadFile(
|
||
file: File,
|
||
type: string = 'generic',
|
||
metadata?: Record<string, unknown>,
|
||
accessMode?: number
|
||
): Promise<Upload> {
|
||
const formData = new FormData()
|
||
formData.append('file', file)
|
||
formData.append('type', type)
|
||
if (metadata) {
|
||
formData.append('metadata', JSON.stringify(metadata))
|
||
}
|
||
if (accessMode !== undefined) {
|
||
formData.append('access_mode', String(accessMode))
|
||
}
|
||
|
||
const response = await apiClient.post<{ data: Upload }>('/api/v1/upload', formData, {
|
||
headers: { 'Content-Type': 'multipart/form-data' },
|
||
} as InternalAxiosRequestConfig)
|
||
return response.data.data
|
||
}
|
||
|
||
/**
|
||
* 获取文件列表
|
||
* @param page - 页码(1-based)
|
||
* @param pageSize - 每页数量
|
||
* @param keyword - 搜索关键词(文件名模糊)
|
||
* @param type - 业务分类过滤
|
||
* @param extension - 扩展名过滤
|
||
*/
|
||
static async listUploads(
|
||
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>('', params)
|
||
}
|
||
|
||
/**
|
||
* 获取当前用户文件统计信息
|
||
*/
|
||
static async getFileStats(): Promise<FileStatsResponse> {
|
||
return this.get<FileStatsResponse>('/stats')
|
||
}
|
||
|
||
/**
|
||
* 删除文件
|
||
*/
|
||
static async deleteFile(id: string): Promise<void> {
|
||
return this.delete<void>(`/${id}`)
|
||
}
|
||
|
||
/**
|
||
* 获取单文件下载 URL(触发 attachment 下载)
|
||
*/
|
||
static getDownloadUrl(id: string): string {
|
||
return `/api/v1/admin/uploads/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: string = 'generic',
|
||
filename: string = 'image.png',
|
||
accessMode?: number
|
||
): Promise<UploadImageResponse> {
|
||
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, undefined, accessMode)
|
||
return { id: result.id }
|
||
}
|
||
|
||
/**
|
||
* 获取我的文件列表
|
||
*/
|
||
static async listMyUploads(
|
||
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
|
||
const response = await apiClient.get<{ data: ListUploadsResponse }>('/api/v1/upload/my', {
|
||
params
|
||
} as InternalAxiosRequestConfig)
|
||
return response.data.data
|
||
}
|
||
|
||
|
||
/**
|
||
* 删除我的文件
|
||
*/
|
||
static async deleteMyFile(id: string): Promise<void> {
|
||
const response = await apiClient.delete<{ data: void }>(`/api/v1/upload/${id}`)
|
||
return response.data.data
|
||
}
|
||
|
||
/**
|
||
* 更新我的文件
|
||
*/
|
||
static async updateMyFile(id: string, fileName: string, accessMode?: number): Promise<Upload> {
|
||
const response = await apiClient.put<{ data: Upload }>(`/api/v1/upload/${id}`, {
|
||
file_name: fileName,
|
||
access_mode: accessMode
|
||
})
|
||
return response.data.data
|
||
}
|
||
|
||
/**
|
||
* 我的文件批量 ZIP 打包下载
|
||
*/
|
||
static async batchDownloadMyFiles(ids: string[]): Promise<Blob> {
|
||
const response = await apiClient.post<Blob>('/api/v1/upload/download/batch', { ids }, {
|
||
responseType: 'blob',
|
||
} as InternalAxiosRequestConfig)
|
||
return response.data
|
||
}
|
||
}
|