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) => 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 const detail = record.detail if (typeof detail === 'string') return detail if (typeof detail === 'object' && detail) { const detailRecord = detail as Record const nestedError = detailRecord.error if (typeof nestedError === 'object' && nestedError) { const message = (nestedError as Record).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).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(path: string, init: RequestInit = {}): Promise { 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 { 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 => request('/projects/', { method: 'GET' }) export const getProject = (id: string): Promise => request(`/projects/${encodeURIComponent(id)}/`, { method: 'GET' }) export const deleteProject = (id: string): Promise => request(`/projects/${encodeURIComponent(id)}/`, { method: 'DELETE' }) export const createProject = (payload: { name: string; description?: string }): Promise => request('/projects/', { method: 'POST', body: JSON.stringify(payload) }) /** 项目数据上传(multipart 字段名需与后端一致) */ export const uploadProjectFile = async ( projectId: string, file: File, options?: { groupIndex?: string; wavenumberRange?: string } ): Promise => { 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 => request(`/projects/${encodeURIComponent(projectId)}/data/${fileId}/select/`, { method: 'POST', body: JSON.stringify({}) }) /** 删除项目下的数据文件 */ export const deleteProjectDataFile = ( projectId: string, fileId: number ): Promise => request(`/projects/${encodeURIComponent(projectId)}/data/${fileId}/delete/`, { method: 'DELETE' }) export type ProjectAnalysisRequest = Record export const getProjectAnalysisResults = (projectId: string): Promise => request(`/projects/${encodeURIComponent(projectId)}/analysis/results/`, { method: 'GET' }) export const runProjectAnalysis = ( projectId: string, payload: ProjectAnalysisRequest, workflow?: unknown ): Promise => { 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 => requestBlob(`/projects/${encodeURIComponent(projectId)}/results/export/${encodeURIComponent(exportId)}/`, { method: 'GET' }) export const downloadProjectAnalysisExport = (projectId: string): Promise => 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' })