RamEx-Flow / front /src /api /http.ts
Aye10032
feat: Support workflow payloads and preview downloads
3933dfe
Raw
History Blame Contribute Delete
7.86 kB
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' })