Spaces:
Sleeping
Sleeping
| import { i18n } from '../i18n' | |
| export const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL ?? 'http://180.76.247.146:18724/api').replace(/\/$/, '') | |
| const t = (key: string, params?: Record<string, unknown>) => i18n.global.t(key, params ?? {}) | |
| export class ApiError extends Error { | |
| status: number | |
| data: unknown | |
| constructor(message: string, status: number, data: unknown) { | |
| super(message) | |
| this.name = 'ApiError' | |
| this.status = status | |
| this.data = data | |
| } | |
| } | |
| function errorMessage(data: unknown, status: number): string { | |
| if (typeof data === 'object' && data) { | |
| const record = data as Record<string, unknown> | |
| const detail = record.detail | |
| if (typeof detail === 'string') return detail | |
| if (typeof detail === 'object' && detail) { | |
| const detailRecord = detail as Record<string, unknown> | |
| const nestedError = detailRecord.error | |
| if (typeof nestedError === 'object' && nestedError) { | |
| const message = (nestedError as Record<string, unknown>).message | |
| if (typeof message === 'string') return message | |
| } | |
| const message = detailRecord.message | |
| if (typeof message === 'string') return message | |
| } | |
| const nestedError = record.error | |
| if (typeof nestedError === 'object' && nestedError) { | |
| const message = (nestedError as Record<string, unknown>).message | |
| if (typeof message === 'string') return message | |
| } | |
| const message = record.message | |
| if (typeof message === 'string') return message | |
| } | |
| return t('common.requestFailed', { status }) | |
| } | |
| /** 项目关联的数据文件(详情接口) */ | |
| export interface BackendDataFile { | |
| id: number | string | |
| filename?: string | |
| /** 旧字段兼容 */ | |
| name?: string | |
| uploaded_at?: string | |
| created_at?: string | |
| wavenumber_min?: number | |
| wavenumber_max?: number | |
| size?: number | |
| } | |
| export interface BackendProject { | |
| id: string | |
| name: string | |
| description?: string | |
| created_at: string | |
| last_modified?: string | |
| status: string | |
| data_files?: BackendDataFile[] | |
| files?: BackendDataFile[] | |
| /** 当前作为分析使用的数据文件 */ | |
| active_data_file?: BackendDataFile | null | |
| data_files_count?: number | |
| file_count?: number | |
| files_count?: number | |
| } | |
| async function request<T>(path: string, init: RequestInit = {}): Promise<T> { | |
| const headers = new Headers(init.headers ?? {}) | |
| if (!headers.has('Content-Type') && !(init.body instanceof FormData)) { | |
| headers.set('Content-Type', 'application/json') | |
| } | |
| headers.set('accept', 'application/json') | |
| const res = await fetch(`${API_BASE_URL}${path}`, { | |
| ...init, | |
| headers | |
| }) | |
| const contentType = res.headers.get('content-type') ?? '' | |
| const isJson = contentType.includes('application/json') | |
| const data = isJson ? await res.json() : await res.text() | |
| if (!res.ok) { | |
| throw new ApiError(errorMessage(data, res.status), res.status, data) | |
| } | |
| return data as T | |
| } | |
| async function requestBlob(path: string, init: RequestInit = {}): Promise<Blob> { | |
| const headers = new Headers(init.headers ?? {}) | |
| headers.set('accept', '*/*') | |
| const res = await fetch(`${API_BASE_URL}${path}`, { | |
| ...init, | |
| headers | |
| }) | |
| if (!res.ok) { | |
| const contentType = res.headers.get('content-type') ?? '' | |
| const isJson = contentType.includes('application/json') | |
| const data = isJson ? await res.json() : await res.text() | |
| throw new ApiError(errorMessage(data, res.status), res.status, data) | |
| } | |
| return await res.blob() | |
| } | |
| function parseFilenameFromContentDisposition(value: string | null): string { | |
| if (!value) return '' | |
| // 优先解析 RFC5987 filename*=UTF-8''xxx | |
| const star = /filename\*\s*=\s*([^;]+)/i.exec(value) | |
| if (star?.[1]) { | |
| const v = star[1].trim().replace(/^["']|["']$/g, '') | |
| const parts = v.split("''") | |
| const encoded = parts.length >= 2 ? parts.slice(1).join("''") : v | |
| try { | |
| return decodeURIComponent(encoded) | |
| } catch { | |
| return encoded | |
| } | |
| } | |
| // 再解析 filename="xxx" | |
| const normal = /filename\s*=\s*([^;]+)/i.exec(value) | |
| if (normal?.[1]) return normal[1].trim().replace(/^["']|["']$/g, '') | |
| return '' | |
| } | |
| async function requestBlobWithFilename( | |
| path: string, | |
| init: RequestInit = {} | |
| ): Promise<{ blob: Blob; filename: string }> { | |
| const headers = new Headers(init.headers ?? {}) | |
| headers.set('accept', '*/*') | |
| const res = await fetch(`${API_BASE_URL}${path}`, { | |
| ...init, | |
| headers | |
| }) | |
| if (!res.ok) { | |
| const contentType = res.headers.get('content-type') ?? '' | |
| const isJson = contentType.includes('application/json') | |
| const data = isJson ? await res.json() : await res.text() | |
| throw new ApiError(errorMessage(data, res.status), res.status, data) | |
| } | |
| const filename = parseFilenameFromContentDisposition(res.headers.get('content-disposition')) | |
| const blob = await res.blob() | |
| return { blob, filename } | |
| } | |
| export const getProjects = (): Promise<BackendProject[]> => | |
| request<BackendProject[]>('/projects/', { method: 'GET' }) | |
| export const getProject = (id: string): Promise<BackendProject> => | |
| request<BackendProject>(`/projects/${encodeURIComponent(id)}/`, { method: 'GET' }) | |
| export const deleteProject = (id: string): Promise<unknown> => | |
| request(`/projects/${encodeURIComponent(id)}/`, { method: 'DELETE' }) | |
| export const createProject = (payload: { name: string; description?: string }): Promise<BackendProject> => | |
| request<BackendProject>('/projects/', { | |
| method: 'POST', | |
| body: JSON.stringify(payload) | |
| }) | |
| /** 项目数据上传(multipart 字段名需与后端一致) */ | |
| export const uploadProjectFile = async ( | |
| projectId: string, | |
| file: File, | |
| options?: { groupIndex?: string; wavenumberRange?: string } | |
| ): Promise<unknown> => { | |
| const formData = new FormData() | |
| formData.append('files', file) | |
| const gi = options?.groupIndex?.trim() | |
| const wr = options?.wavenumberRange?.trim() | |
| if (gi) formData.append('group_index', gi) | |
| if (wr) formData.append('Wavenumber range', wr) | |
| return request(`/projects/${encodeURIComponent(projectId)}/data/upload/`, { | |
| method: 'POST', | |
| body: formData | |
| }) | |
| } | |
| /** 将指定数据文件设为当前活动文件 */ | |
| export const setProjectActiveDataFile = ( | |
| projectId: string, | |
| fileId: number | |
| ): Promise<unknown> => | |
| request(`/projects/${encodeURIComponent(projectId)}/data/${fileId}/select/`, { | |
| method: 'POST', | |
| body: JSON.stringify({}) | |
| }) | |
| /** 删除项目下的数据文件 */ | |
| export const deleteProjectDataFile = ( | |
| projectId: string, | |
| fileId: number | |
| ): Promise<unknown> => | |
| request(`/projects/${encodeURIComponent(projectId)}/data/${fileId}/delete/`, { | |
| method: 'DELETE' | |
| }) | |
| export type ProjectAnalysisRequest = Record<string, any> | |
| export const getProjectAnalysisResults = (projectId: string): Promise<unknown> => | |
| request(`/projects/${encodeURIComponent(projectId)}/analysis/results/`, { method: 'GET' }) | |
| export const runProjectAnalysis = ( | |
| projectId: string, | |
| payload: ProjectAnalysisRequest, | |
| workflow?: unknown | |
| ): Promise<unknown> => { | |
| const form = new FormData() | |
| form.append('params', JSON.stringify(payload)) | |
| if (workflow) form.append('workflow', JSON.stringify(workflow)) | |
| return request(`/projects/${encodeURIComponent(projectId)}/analysis/`, { | |
| method: 'POST', | |
| body: form | |
| }) | |
| } | |
| export const downloadProjectResultsExport = (projectId: string, exportId: string): Promise<Blob> => | |
| requestBlob(`/projects/${encodeURIComponent(projectId)}/results/export/${encodeURIComponent(exportId)}/`, { method: 'GET' }) | |
| export const downloadProjectAnalysisExport = (projectId: string): Promise<Blob> => | |
| requestBlob(`/projects/${encodeURIComponent(projectId)}/analysis/export/`, { method: 'GET' }) | |
| export const downloadProjectAnalysisExportFile = (projectId: string): Promise<{ blob: Blob; filename: string }> => | |
| requestBlobWithFilename(`/projects/${encodeURIComponent(projectId)}/analysis/export/`, { method: 'GET' }) | |