mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-12 02:06:37 +08:00
refactor(frontend): colocate admin components and split service layer
Move route-specific admin components from components/common/admin into app/(main)/admin/<feature>/components/. Split AdminService god object into domain services, fix UploadService to use BaseService, add AdminUploadService, consolidate DB export into DbManageService, and migrate consumers to the unified services entry.
This commit is contained in:
@@ -1,189 +1,78 @@
|
||||
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'
|
||||
import type {InternalAxiosRequestConfig} from 'axios';
|
||||
|
||||
export type ImageQuality = 'low' | 'medium' | 'high' | 'origin'
|
||||
import {BaseService} from '../core/base.service';
|
||||
import type {ListUploadsResponse, Upload, UploadImageResponse} from './types';
|
||||
|
||||
/**
|
||||
* 根据上传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'
|
||||
protected static readonly basePath = '/api/v1/upload';
|
||||
|
||||
/**
|
||||
* 通用文件上传
|
||||
* @param file - 文件对象
|
||||
* @param type - 业务分类(如 avatar、attachment、generic)
|
||||
* @param metadata - 可选额外 JSON 元数据
|
||||
*/
|
||||
static async uploadFile(
|
||||
file: File,
|
||||
type: string = 'generic',
|
||||
metadata?: Record<string, unknown>,
|
||||
accessMode?: number
|
||||
accessMode?: number,
|
||||
): Promise<Upload> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('type', type)
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('type', type);
|
||||
if (metadata) {
|
||||
formData.append('metadata', JSON.stringify(metadata))
|
||||
formData.append('metadata', JSON.stringify(metadata));
|
||||
}
|
||||
if (accessMode !== undefined) {
|
||||
formData.append('access_mode', String(accessMode))
|
||||
formData.append('access_mode', String(accessMode));
|
||||
}
|
||||
|
||||
const response = await apiClient.post<{ data: Upload }>('/api/v1/upload', formData, {
|
||||
return this.post<Upload>('', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
} as InternalAxiosRequestConfig)
|
||||
return response.data.data
|
||||
} as InternalAxiosRequestConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件列表
|
||||
* @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
|
||||
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 }
|
||||
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
|
||||
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
|
||||
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 deleteMyFile(id: string): Promise<void> {
|
||||
const response = await apiClient.delete<{ data: void }>(`/api/v1/upload/${id}`)
|
||||
return response.data.data
|
||||
return this.delete<void>(`/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新我的文件
|
||||
*/
|
||||
static async updateMyFile(id: string, fileName: string, accessMode?: number): Promise<Upload> {
|
||||
const response = await apiClient.put<{ data: Upload }>(`/api/v1/upload/${id}`, {
|
||||
return this.put<Upload>(`/${id}`, {
|
||||
file_name: fileName,
|
||||
access_mode: accessMode
|
||||
})
|
||||
return response.data.data
|
||||
access_mode: accessMode,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的文件批量 ZIP 打包下载
|
||||
*/
|
||||
static async batchDownloadMyFiles(ids: string[]): Promise<Blob> {
|
||||
const response = await apiClient.post<Blob>('/api/v1/upload/download/batch', { ids }, {
|
||||
return this.post<Blob>('/download/batch', { ids }, {
|
||||
responseType: 'blob',
|
||||
} as InternalAxiosRequestConfig)
|
||||
return response.data
|
||||
} as InternalAxiosRequestConfig);
|
||||
}
|
||||
}
|
||||
|
||||
static getDownloadUrl(id: string): string {
|
||||
return `${this.basePath}/download/${id}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user