68 lines
6.7 KiB
TypeScript
68 lines
6.7 KiB
TypeScript
import { http, unwrapApiResponse, ApiResponseError, type ApiEnvelope } from '@/shared/api/http'
|
|||
|
|
import axios from 'axios'
|
||
|
|
import type { PageResult } from './overseasMasterData'
|
||
|
|
|
||
|
|
export type FleetQuoteKind = 'PURCHASE' | 'SALE'
|
||
|
|
export interface FleetQuoteDraft {
|
||
|
|
kind: FleetQuoteKind; partyId: string; originCity: string; destinationCode: string; distanceMiles: number
|
||
|
|
currency: 'USD' | 'CNY'; selfPickupPrice: number | null; fullDeliveryPrice: number | null; looseDeliveryPrice: number | null
|
||
|
|
validFrom: string; validTo: string; remark: string | null
|
||
|
|
}
|
||
|
|
export interface FleetQuote { quoteNo: string; details: FleetQuoteDraft; partyName: string; status: 'ACTIVE' | 'INACTIVE' | 'DELETED'; recordVersion: string; updatedAt: string }
|
||
|
|
export interface FleetQuoteQuery { page?: number; size?: number; keyword?: string; kind?: FleetQuoteKind; status?: string; effectiveOn?: string }
|
||
|
|
type Numeric = string | number
|
||
|
|
type WireQuote = Omit<FleetQuote, 'details' | 'recordVersion'> & { recordVersion: Numeric; details: Omit<FleetQuoteDraft, 'distanceMiles' | 'selfPickupPrice' | 'fullDeliveryPrice' | 'looseDeliveryPrice'> & { distanceMiles: Numeric; selfPickupPrice: Numeric | null; fullDeliveryPrice: Numeric | null; looseDeliveryPrice: Numeric | null } }
|
||
|
|
export function fleetNumber(value: Numeric): number {
|
||
|
|
if ((typeof value !== 'string' && typeof value !== 'number') || String(value).trim() === '' || !Number.isFinite(Number(value)) || Number(value) < 0) throw new Error('接口返回了无效数值')
|
||
|
|
return Number(value)
|
||
|
|
}
|
||
|
|
export function fleetMoney(value: Numeric): number {
|
||
|
|
const text = String(value)
|
||
|
|
if (!/^\d+(\.\d{1,2})?$/.test(text)) throw new Error('金额格式无效')
|
||
|
|
const [whole, fraction = ''] = text.split('.')
|
||
|
|
const cents = BigInt(whole!) * 100n + BigInt(fraction.padEnd(2, '0'))
|
||
|
|
if (cents > BigInt(Number.MAX_SAFE_INTEGER) || BigInt(Number(value).toFixed(2).replace('.', '')) !== cents) throw new Error('金额超出精确展示范围')
|
||
|
|
return Number(value)
|
||
|
|
}
|
||
|
|
export function fleetVersion(value: Numeric): string {
|
||
|
|
if (!/^(0|[1-9]\d*)$/.test(String(value)) || (typeof value === 'number' && !Number.isSafeInteger(value))) throw new Error('接口版本无效')
|
||
|
|
return String(value)
|
||
|
|
}
|
||
|
|
export function fleetPage<T>(value: { records: T[]; page: Numeric; size: Numeric; total: Numeric; pages: Numeric }): PageResult<T> {
|
||
|
|
const integer = (n: Numeric) => { const result = fleetNumber(n); if (!Number.isSafeInteger(result)) throw new Error('接口分页超出有效范围'); return result }
|
||
|
|
return { ...value, page: integer(value.page), size: integer(value.size), total: integer(value.total), pages: integer(value.pages) }
|
||
|
|
}
|
||
|
|
const base = '/platform/v1/last-leg/fleet/quotes'
|
||
|
|
const path = (no: string) => `${base}/${encodeURIComponent(no)}`
|
||
|
|
const quote = (v: WireQuote): FleetQuote => ({ ...v, recordVersion: fleetVersion(v.recordVersion), details: { ...v.details, partyId: String(v.details.partyId), distanceMiles: fleetNumber(v.details.distanceMiles),
|
||
|
|
selfPickupPrice: v.details.selfPickupPrice == null ? null : fleetNumber(v.details.selfPickupPrice), fullDeliveryPrice: v.details.fullDeliveryPrice == null ? null : fleetNumber(v.details.fullDeliveryPrice), looseDeliveryPrice: v.details.looseDeliveryPrice == null ? null : fleetNumber(v.details.looseDeliveryPrice) } })
|
||
|
|
export async function listFleetQuotes(query: FleetQuoteQuery) {
|
||
|
|
const result = unwrapApiResponse(await http.get<ApiEnvelope<PageResult<WireQuote>>, ApiEnvelope<PageResult<WireQuote>>>(base, { params: query }))
|
||
|
|
return fleetPage({ ...result, records: result.records.map(quote) })
|
||
|
|
}
|
||
|
|
export async function createFleetQuote(idempotencyKey: string, details: FleetQuoteDraft) { return quote(unwrapApiResponse(await http.post<ApiEnvelope<WireQuote>, ApiEnvelope<WireQuote>>(base, { idempotencyKey, details }))) }
|
||
|
|
export async function updateFleetQuote(value: FleetQuote, details: FleetQuoteDraft) { return quote(unwrapApiResponse(await http.put<ApiEnvelope<WireQuote>, ApiEnvelope<WireQuote>>(path(value.quoteNo), { recordVersion: value.recordVersion, details }))) }
|
||
|
|
export async function setFleetQuoteStatus(value: FleetQuote, status: string) { return quote(unwrapApiResponse(await http.patch<ApiEnvelope<WireQuote>, ApiEnvelope<WireQuote>>(`${path(value.quoteNo)}/status`, { recordVersion: value.recordVersion, status }))) }
|
||
|
|
export async function deleteFleetQuote(value: FleetQuote) { return quote(unwrapApiResponse(await http.delete<ApiEnvelope<WireQuote>, ApiEnvelope<WireQuote>>(path(value.quoteNo), { params: { recordVersion: value.recordVersion } }))) }
|
||
|
|
export async function importFleetQuotes(idempotencyKey: string, rows: FleetQuoteDraft[]) { return unwrapApiResponse(await http.post<ApiEnvelope<{ importedRows: number }>, ApiEnvelope<{ importedRows: number }>>(`${base}/import`, { idempotencyKey, rows })) }
|
||
|
|
export async function fleetQuoteCustomers(keyword: string) { return unwrapApiResponse(await http.get<ApiEnvelope<{ id: string; name: string; code: string }[]>, ApiEnvelope<{ id: string; name: string; code: string }[]>>(`${base}/customer-options`, { params: { keyword } })) }
|
||
|
|
export async function fleetQuoteSuppliers(keyword: string) {
|
||
|
|
const result = unwrapApiResponse(await http.get<ApiEnvelope<PageResult<{ id: string; name: string }>>, ApiEnvelope<PageResult<{ id: string; name: string }>>>('/platform/v1/suppliers', { params: { page: 1, size: 30, keyword, status: 'ACTIVE', category: 'TRUCKING', truckingScope: 'OVERSEAS' } }))
|
||
|
|
return result.records.map(row => ({ ...row, id: String(row.id) }))
|
||
|
|
}
|
||
|
|
export async function fleetCsv(path: string, params: object): Promise<Blob> {
|
||
|
|
function check(bytes: ArrayBuffer, status: number) {
|
||
|
|
const text = new TextDecoder().decode(bytes)
|
||
|
|
if (!/^\s*[{[]/.test(text)) return
|
||
|
|
let error: ApiEnvelope<unknown>
|
||
|
|
try { error = JSON.parse(text) as ApiEnvelope<unknown> } catch { throw new ApiResponseError('导出接口返回了无效响应', 'INVALID_EXPORT_RESPONSE', status) }
|
||
|
|
throw new ApiResponseError(error.message || '导出失败', error.code || 'EXPORT_FAILED', status, error.data, error.requestId, error.traceId)
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const bytes = await http.get<ArrayBuffer, ArrayBuffer>(path, { params, responseType: 'arraybuffer' }); check(bytes, 200)
|
||
|
|
return new Blob([bytes], { type: 'text/csv;charset=utf-8' })
|
||
|
|
} catch (e) { if (axios.isAxiosError(e) && Object.prototype.toString.call(e.response?.data) === '[object ArrayBuffer]') check(e.response!.data as ArrayBuffer, e.response!.status); throw e }
|
||
|
|
}
|
||
|
|
export const exportFleetQuotes = (query: FleetQuoteQuery) => fleetCsv(`${base}/export`, query)
|
||
|
|
export function saveFleetCsv(blob: Blob, name: string) { const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = name; anchor.click(); setTimeout(() => URL.revokeObjectURL(url), 1000) }
|