Files
admin-web/src/modules/overseas-master-data/api/overseasMasterData.ts
T

125 lines
12 KiB
TypeScript
Raw Normal View History

import axios from 'axios'
2026-08-20 18:02:52 +08:00
import { overseasHttp } from '@/shared/api/overseasHttp'
import { ApiResponseError, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
2026-08-20 18:02:52 +08:00
import type {
MasterDataStatus, MonthlyExchangeRate, OverseasTransitPrice, OverseasWarehouseFee, OverseasWarehouseQuote, OverseasWarehouseQuoteDetail, PartnerWarehouseOption,
2026-08-20 18:02:52 +08:00
VehicleDetail, VehicleOwnership, VehicleRegion, VehicleStatus,
} from '../types'
export interface PageResult<T> { records: T[]; page: number; size: number; total: number; pages: number }
type PagePayload<T> = Omit<PageResult<T>, '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<OverseasWarehouseQuote, 'recordVersion'> & { recordVersion: Numeric }
type FeeWire = Omit<OverseasWarehouseFee, 'unitPrice' | 'minimumCharge'> & { unitPrice: Numeric; minimumCharge: Numeric | null }
type TransitPriceWire = Omit<OverseasTransitPrice, 'estimatedMiles' | 'price' | 'minimumCharge'> & { estimatedMiles: Numeric | null; price: Numeric; minimumCharge: Numeric | null }
type QuoteDetailWire = Omit<OverseasWarehouseQuoteDetail, 'summary' | 'warehouseFees' | 'transitPrices'> & { summary: QuoteWire; warehouseFees: FeeWire[]; transitPrices: TransitPriceWire[] }
type ExchangeRateWire = Omit<MonthlyExchangeRate, 'usdToCnyAverage' | 'recordVersion'> & { usdToCnyAverage: Numeric; recordVersion: Numeric }
type ExchangeRateSummary = { latestMonth: string | null; latestUsdToCnyAverage: number | null; updatedBy: string | null; updatedAt: string | null }
type ExchangeRateSummaryWire = Omit<ExchangeRateSummary, 'latestUsdToCnyAverage'> & { latestUsdToCnyAverage: Numeric | null }
type VehicleWire = Omit<VehicleDetail, 'fuelConsumptionPer100Km' | 'mileageKm' | 'recordVersion'> & { fuelConsumptionPer100Km: Numeric; mileageKm: Numeric; recordVersion: Numeric }
function page<T, W>(value: PagePayload<W>, normalize: (record: W) => T): PageResult<T> {
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<T, W>(path: string, params: object, normalize: (record: W) => T) {
return page(unwrapApiResponse(await overseasHttp.get<ApiEnvelope<PagePayload<W>>, ApiEnvelope<PagePayload<W>>>(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<ApiEnvelope<unknown>>
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 overseasHttp.get<DownloadPayload, DownloadPayload>(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
}
}
2026-08-20 18:02:52 +08:00
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<OverseasWarehouseQuote, 'name' | 'providerName' | 'currency' | 'effectiveStart' | 'remark'>
& { effectiveEnd: string } & QuoteDetailFields
export type QuoteCreate = QuoteEditableFields & Pick<OverseasWarehouseQuote, 'warehouseId' | 'versionCode'>
export type QuoteUpdate = QuoteEditableFields & Pick<OverseasWarehouseQuote, 'recordVersion'>
export const listQuotes = (query: QuoteQuery) => getPage<OverseasWarehouseQuote, QuoteWire>(`${base}/overseas-quotes`, query, normalizeQuote)
export async function getQuote(id: string) { return normalizeQuoteDetail(unwrapApiResponse(await overseasHttp.get<ApiEnvelope<QuoteDetailWire>, ApiEnvelope<QuoteDetailWire>>(`${base}/overseas-quotes/${encodeURIComponent(id)}`))) }
2026-08-20 18:02:52 +08:00
export async function listQuoteWarehouseOptions() { return unwrapApiResponse(await overseasHttp.get<ApiEnvelope<PartnerWarehouseOption[]>, ApiEnvelope<PartnerWarehouseOption[]>>(`${base}/overseas-quotes/warehouse-options`)) }
export async function createQuote(payload: QuoteCreate) { return normalizeQuoteDetail(unwrapApiResponse(await overseasHttp.post<ApiEnvelope<QuoteDetailWire>, ApiEnvelope<QuoteDetailWire>>(`${base}/overseas-quotes`, payload))) }
export async function updateQuote(id: string, payload: QuoteUpdate) { return normalizeQuoteDetail(unwrapApiResponse(await overseasHttp.put<ApiEnvelope<QuoteDetailWire>, ApiEnvelope<QuoteDetailWire>>(`${base}/overseas-quotes/${encodeURIComponent(id)}`, payload))) }
export async function changeQuoteStatus(id: string, status: MasterDataStatus, recordVersion: number) { return normalizeQuoteDetail(unwrapApiResponse(await overseasHttp.patch<ApiEnvelope<QuoteDetailWire>, ApiEnvelope<QuoteDetailWire>>(`${base}/overseas-quotes/${encodeURIComponent(id)}/status`, { status, recordVersion }))) }
2026-08-20 18:02:52 +08:00
export const exportQuotes = (query: Omit<QuoteQuery, 'page' | 'size'>) => 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<MonthlyExchangeRate, ExchangeRateWire>(`${base}/exchange-rates`, query, normalizeExchangeRate)
export async function getExchangeRate(month: string) { return normalizeExchangeRate(unwrapApiResponse(await overseasHttp.get<ApiEnvelope<ExchangeRateWire>, ApiEnvelope<ExchangeRateWire>>(`${base}/exchange-rates/${encodeURIComponent(month)}`))) }
export async function getExchangeRateSummary(): Promise<ExchangeRateSummary> {
const value = unwrapApiResponse(await overseasHttp.get<ApiEnvelope<ExchangeRateSummaryWire>, ApiEnvelope<ExchangeRateSummaryWire>>(`${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 overseasHttp.post<ApiEnvelope<ExchangeRateWire>, ApiEnvelope<ExchangeRateWire>>(`${base}/exchange-rates`, payload))) }
export async function updateExchangeRate(month: string, payload: { recordVersion: number; usdToCnyAverage: number; remark: string | null }) { return normalizeExchangeRate(unwrapApiResponse(await overseasHttp.put<ApiEnvelope<ExchangeRateWire>, ApiEnvelope<ExchangeRateWire>>(`${base}/exchange-rates/${encodeURIComponent(month)}`, payload))) }
export async function changeExchangeRateStatus(month: string, status: MasterDataStatus, recordVersion: number, remark: string | null) { return normalizeExchangeRate(unwrapApiResponse(await overseasHttp.patch<ApiEnvelope<ExchangeRateWire>, ApiEnvelope<ExchangeRateWire>>(`${base}/exchange-rates/${encodeURIComponent(month)}/status`, { status, recordVersion, remark }))) }
2026-08-20 18:02:52 +08:00
export const exportExchangeRates = (query: Omit<ExchangeRateQuery, 'page' | 'size'>) => download(`${base}/exchange-rates/export`, query)
export interface VehicleQuery { page: number; size: number; keyword?: string; ownership?: VehicleOwnership; region?: VehicleRegion; status?: VehicleStatus }
type VehicleEditableFields = Omit<VehicleDetail, 'id' | 'recordVersion' | 'updatedBy' | 'updatedAt' | 'status'>
export type VehicleCreate = VehicleEditableFields & Pick<VehicleDetail, 'status'>
export type VehicleUpdate = VehicleEditableFields & Pick<VehicleDetail, 'recordVersion'>
export const listVehicles = (query: VehicleQuery) => getPage<VehicleDetail, VehicleWire>(`${base}/vehicles`, query, normalizeVehicle)
export async function getVehicle(id: string) { return normalizeVehicle(unwrapApiResponse(await overseasHttp.get<ApiEnvelope<VehicleWire>, ApiEnvelope<VehicleWire>>(`${base}/vehicles/${encodeURIComponent(id)}`))) }
export async function createVehicle(payload: VehicleCreate) { return normalizeVehicle(unwrapApiResponse(await overseasHttp.post<ApiEnvelope<VehicleWire>, ApiEnvelope<VehicleWire>>(`${base}/vehicles`, payload))) }
export async function updateVehicle(id: string, payload: VehicleUpdate) { return normalizeVehicle(unwrapApiResponse(await overseasHttp.put<ApiEnvelope<VehicleWire>, ApiEnvelope<VehicleWire>>(`${base}/vehicles/${encodeURIComponent(id)}`, payload))) }
export async function changeVehicleStatus(id: string, status: VehicleStatus, recordVersion: number) { return normalizeVehicle(unwrapApiResponse(await overseasHttp.patch<ApiEnvelope<VehicleWire>, ApiEnvelope<VehicleWire>>(`${base}/vehicles/${encodeURIComponent(id)}/status`, { status, recordVersion }))) }
2026-08-20 18:02:52 +08:00
export const exportVehicles = (query: Omit<VehicleQuery, 'page' | 'size'>) => download(`${base}/vehicles/export`, query)