import axios from 'axios' import { http, ApiResponseError, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' import type { MasterDataStatus, MonthlyExchangeRate, OverseasTransitPrice, OverseasWarehouseFee, OverseasWarehouseQuote, OverseasWarehouseQuoteDetail, PartnerWarehouseOption, VehicleDetail, VehicleOwnership, VehicleRegion, VehicleStatus, } from '../types' export interface PageResult { records: T[]; page: number; size: number; total: number; pages: number } type PagePayload = Omit, 'page' | 'size' | 'total' | 'pages'> & { page: number | string; size: number | string; total: number | string; pages: number | string } const base = '/platform/v1/last-leg/master-data' type Numeric = number | string type QuoteWire = Omit & { recordVersion: Numeric } type FeeWire = Omit & { unitPrice: Numeric; minimumCharge: Numeric | null } type TransitPriceWire = Omit & { estimatedMiles: Numeric | null; price: Numeric; minimumCharge: Numeric | null } type QuoteDetailWire = Omit & { summary: QuoteWire; warehouseFees: FeeWire[]; transitPrices: TransitPriceWire[] } type ExchangeRateWire = Omit & { usdToCnyAverage: Numeric; recordVersion: Numeric } type ExchangeRateSummary = { latestMonth: string | null; latestUsdToCnyAverage: number | null; updatedBy: string | null; updatedAt: string | null } type ExchangeRateSummaryWire = Omit & { latestUsdToCnyAverage: Numeric | null } type VehicleWire = Omit & { fuelConsumptionPer100Km: Numeric; mileageKm: Numeric; recordVersion: Numeric } function page(value: PagePayload, normalize: (record: W) => T): PageResult { return { ...value, records: value.records.map(normalize), page: Number(value.page), size: Number(value.size), total: Number(value.total), pages: Number(value.pages) } } async function getPage(path: string, params: object, normalize: (record: W) => T) { return page(unwrapApiResponse(await http.get>, ApiEnvelope>>(path, { params })), normalize) } const numeric = (value: Numeric) => Number(value) const nullableNumeric = (value: Numeric | null) => value == null ? null : Number(value) const normalizeQuote = (value: QuoteWire): OverseasWarehouseQuote => ({ ...value, recordVersion: numeric(value.recordVersion) }) const normalizeFee = (value: FeeWire): OverseasWarehouseFee => ({ ...value, unitPrice: numeric(value.unitPrice), minimumCharge: nullableNumeric(value.minimumCharge) }) const normalizeTransitPrice = (value: TransitPriceWire): OverseasTransitPrice => ({ ...value, estimatedMiles: nullableNumeric(value.estimatedMiles), price: numeric(value.price), minimumCharge: nullableNumeric(value.minimumCharge) }) const normalizeQuoteDetail = (value: QuoteDetailWire): OverseasWarehouseQuoteDetail => ({ ...value, summary: normalizeQuote(value.summary), warehouseFees: value.warehouseFees.map(normalizeFee), transitPrices: value.transitPrices.map(normalizeTransitPrice) }) const normalizeExchangeRate = (value: ExchangeRateWire): MonthlyExchangeRate => ({ ...value, usdToCnyAverage: numeric(value.usdToCnyAverage), recordVersion: numeric(value.recordVersion) }) const normalizeVehicle = (value: VehicleWire): VehicleDetail => ({ ...value, fuelConsumptionPer100Km: numeric(value.fuelConsumptionPer100Km), mileageKm: numeric(value.mileageKm), recordVersion: numeric(value.recordVersion) }) type DownloadPayload = Blob | ArrayBuffer | ArrayBufferView | string async function downloadText(payload: DownloadPayload) { if (typeof payload === 'string') return payload if (payload instanceof Blob) return payload.text() const bytes = payload instanceof ArrayBuffer ? new Uint8Array(payload) : new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength) return new TextDecoder().decode(bytes) } async function csvError(payload: DownloadPayload, status: number) { const contentType = payload instanceof Blob ? payload.type.toLowerCase() : '' const text = await downloadText(payload) if (!contentType.includes('json') && !/^\s*[\[{]/.test(text)) return null try { const envelope = JSON.parse(text) as Partial> if (!envelope || typeof envelope !== 'object' || Array.isArray(envelope)) throw new Error('invalid envelope') return new ApiResponseError(envelope.message || 'CSV 导出失败', envelope.code || 'DOWNLOAD_FAILED', status, envelope.data, envelope.requestId, envelope.traceId) } catch (error) { if (error instanceof ApiResponseError) return error return new ApiResponseError('CSV 导出接口返回了无法识别的 JSON 响应', 'INVALID_DOWNLOAD_RESPONSE', status) } } function csvBlob(payload: DownloadPayload) { if (payload instanceof Blob) return payload if (typeof payload === 'string') return new Blob([payload], { type: 'text/csv;charset=utf-8' }) const bytes = payload instanceof ArrayBuffer ? new Uint8Array(payload) : new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength) const copy = new Uint8Array(bytes.byteLength) copy.set(bytes) return new Blob([copy.buffer], { type: 'text/csv;charset=utf-8' }) } async function download(path: string, params: object) { try { const payload = await http.get(path, { params, responseType: 'arraybuffer' }) const error = await csvError(payload, 200) if (error) throw error return csvBlob(payload) } catch (error) { const axiosError = axios.isAxiosError(error) ? error : null const responsePayload = axiosError?.response?.data as DownloadPayload | undefined if (responsePayload !== undefined) { const responseError = await csvError(responsePayload, axiosError?.response?.status || 0) if (responseError) throw responseError } throw error } } export interface QuoteQuery { page: number; size: number; keyword?: string; warehouseId?: string; providerName?: string; currency?: string; status?: MasterDataStatus } type QuoteDetailFields = { warehouseFees: OverseasWarehouseFee[]; transitPrices: OverseasTransitPrice[] } type QuoteEditableFields = Pick & { effectiveEnd: string } & QuoteDetailFields export type QuoteCreate = QuoteEditableFields & Pick export type QuoteUpdate = QuoteEditableFields & Pick export const listQuotes = (query: QuoteQuery) => getPage(`${base}/overseas-quotes`, query, normalizeQuote) export async function getQuote(id: string) { return normalizeQuoteDetail(unwrapApiResponse(await http.get, ApiEnvelope>(`${base}/overseas-quotes/${encodeURIComponent(id)}`))) } export async function listQuoteWarehouseOptions() { return unwrapApiResponse(await http.get, ApiEnvelope>(`${base}/overseas-quotes/warehouse-options`)) } export async function createQuote(payload: QuoteCreate) { return normalizeQuoteDetail(unwrapApiResponse(await http.post, ApiEnvelope>(`${base}/overseas-quotes`, payload))) } export async function updateQuote(id: string, payload: QuoteUpdate) { return normalizeQuoteDetail(unwrapApiResponse(await http.put, ApiEnvelope>(`${base}/overseas-quotes/${encodeURIComponent(id)}`, payload))) } export async function changeQuoteStatus(id: string, status: MasterDataStatus, recordVersion: number) { return normalizeQuoteDetail(unwrapApiResponse(await http.patch, ApiEnvelope>(`${base}/overseas-quotes/${encodeURIComponent(id)}/status`, { status, recordVersion }))) } export const exportQuotes = (query: Omit) => download(`${base}/overseas-quotes/export`, query) export interface ExchangeRateQuery { page: number; size: number; monthFrom?: string; monthTo?: string; status?: MasterDataStatus } export const listExchangeRates = (query: ExchangeRateQuery) => getPage(`${base}/exchange-rates`, query, normalizeExchangeRate) export async function getExchangeRate(month: string) { return normalizeExchangeRate(unwrapApiResponse(await http.get, ApiEnvelope>(`${base}/exchange-rates/${encodeURIComponent(month)}`))) } export async function getExchangeRateSummary(): Promise { const value = unwrapApiResponse(await http.get, ApiEnvelope>(`${base}/exchange-rates/summary`)) return { ...value, latestUsdToCnyAverage: nullableNumeric(value.latestUsdToCnyAverage) } } export async function createExchangeRate(payload: { month: string; usdToCnyAverage: number; remark: string | null }) { return normalizeExchangeRate(unwrapApiResponse(await http.post, ApiEnvelope>(`${base}/exchange-rates`, payload))) } export async function updateExchangeRate(month: string, payload: { recordVersion: number; usdToCnyAverage: number; remark: string | null }) { return normalizeExchangeRate(unwrapApiResponse(await http.put, ApiEnvelope>(`${base}/exchange-rates/${encodeURIComponent(month)}`, payload))) } export async function changeExchangeRateStatus(month: string, status: MasterDataStatus, recordVersion: number, remark: string | null) { return normalizeExchangeRate(unwrapApiResponse(await http.patch, ApiEnvelope>(`${base}/exchange-rates/${encodeURIComponent(month)}/status`, { status, recordVersion, remark }))) } export const exportExchangeRates = (query: Omit) => download(`${base}/exchange-rates/export`, query) export interface VehicleQuery { page: number; size: number; keyword?: string; ownership?: VehicleOwnership; region?: VehicleRegion; status?: VehicleStatus } type VehicleEditableFields = Omit export type VehicleCreate = VehicleEditableFields & Pick export type VehicleUpdate = VehicleEditableFields & Pick export const listVehicles = (query: VehicleQuery) => getPage(`${base}/vehicles`, query, normalizeVehicle) export async function getVehicle(id: string) { return normalizeVehicle(unwrapApiResponse(await http.get, ApiEnvelope>(`${base}/vehicles/${encodeURIComponent(id)}`))) } export async function createVehicle(payload: VehicleCreate) { return normalizeVehicle(unwrapApiResponse(await http.post, ApiEnvelope>(`${base}/vehicles`, payload))) } export async function updateVehicle(id: string, payload: VehicleUpdate) { return normalizeVehicle(unwrapApiResponse(await http.put, ApiEnvelope>(`${base}/vehicles/${encodeURIComponent(id)}`, payload))) } export async function changeVehicleStatus(id: string, status: VehicleStatus, recordVersion: number) { return normalizeVehicle(unwrapApiResponse(await http.patch, ApiEnvelope>(`${base}/vehicles/${encodeURIComponent(id)}/status`, { status, recordVersion }))) } export const exportVehicles = (query: Omit) => download(`${base}/vehicles/export`, query)