144 lines
9.7 KiB
TypeScript
144 lines
9.7 KiB
TypeScript
import type { Amount, ProcurementCreate, ProcurementFee } from './api/procurement'
|
|
|
|
export const procurementStatuses: Record<string, string> = {
|
|
PENDING_REVIEW: '待审核', RECEIVING: '收货中', WAIT_CUSTOMER_CONFIRM: '待客户确认', CONFIRMED: '已确认',
|
|
IN_TRANSIT: '运输中', DELIVERING: '派送中', COMPLETED: '签收完成',
|
|
REJECTED: '已拒绝', CANCELLED: '已取消', EXCEPTION_CLOSED: '异常关闭',
|
|
}
|
|
export const feePurposes: Record<string, string> = {
|
|
SAMPLE_PAYMENT: '样品货款', INTERNATIONAL_ESTIMATE: '预估国际运费', INTERNATIONAL_FREIGHT: '实际国际运费',
|
|
DUTY: '关税', DOMESTIC_FREIGHT: '国内运费', OTHER: '其他费用', ADJUSTMENT: '追加费用', REFUND: '退款',
|
|
}
|
|
export function statusLabel(status: string | null | undefined, direction?: string | null) { return regionText(status ? procurementStatuses[status] || '待核对进度' : '—', direction) }
|
|
export function statusTone(status: string | null | undefined): 'success' | 'warning' | 'danger' | 'info' | 'primary' {
|
|
if (status === 'COMPLETED' || status === 'CONFIRMED') return 'success'
|
|
if (status === 'REJECTED' || status === 'EXCEPTION_CLOSED') return 'danger'
|
|
if (status === 'CANCELLED') return 'info'
|
|
if (status === 'IN_TRANSIT' || status === 'DELIVERING') return 'primary'
|
|
return 'warning'
|
|
}
|
|
function cents(value: Amount | null | undefined): bigint | null {
|
|
if (value === null || value === undefined) return null
|
|
const match = /^(-?)(\d+)(?:\.(\d{1,2}))?$/.exec(String(value).trim())
|
|
if (!match) return null
|
|
const result = BigInt(match[2]) * 100n + BigInt((match[3] || '').padEnd(2, '0'))
|
|
return match[1] ? -result : result
|
|
}
|
|
function decimal(value: bigint) {
|
|
const absolute = value < 0n ? -value : value
|
|
return `${value < 0n ? '-' : ''}${absolute / 100n}.${String(absolute % 100n).padStart(2, '0')}`
|
|
}
|
|
export function money(value: Amount | null | undefined, currency: string | null | undefined = 'CNY') {
|
|
const amount = cents(value)
|
|
return amount === null ? '—' : `${currency || '—'} ${decimal(amount)}`
|
|
}
|
|
export function dateTime(value: string | null | undefined) { return value ? value.replace('T', ' ').replace(/\.\d+$/, '') : '—' }
|
|
export function measurement(value: Amount | null | undefined, scale: number) {
|
|
if (value === null || value === undefined) return '—'
|
|
const parts = /^(\d+)(?:\.(\d+))?$/.exec(String(value))
|
|
if (!parts || (parts[2] || '').length > scale) return '待核对'
|
|
return `${parts[1]}.${(parts[2] || '').padEnd(scale, '0')}`
|
|
}
|
|
export function text(value: unknown) { return value === null || value === undefined || value === '' ? '—' : String(value) }
|
|
export function productHref(value: string | null | undefined) {
|
|
if (!value) return undefined
|
|
try { const url = new URL(value); return ['http:', 'https:'].includes(url.protocol) ? url.href : undefined } catch { return undefined }
|
|
}
|
|
export function feeStatus(fee: ProcurementFee) {
|
|
if (fee.walletStatus === 'NOT_REQUIRED') {
|
|
if (fee.determination === 'PENDING') return '账单结算 · 待审核'
|
|
if (fee.determination === 'APPROVED') return '账单结算 · 已确认'
|
|
if (fee.determination === 'REJECTED') return '账单结算 · 已拒绝'
|
|
return '账单结算'
|
|
}
|
|
if (fee.purpose === 'INTERNATIONAL_ESTIMATE') {
|
|
const label: Record<string, string> = { FROZEN: '已冻结,未扣款', ZERO: '零预估已确认', SETTLED: '原预估已结清', RELEASED: '预估已释放' }
|
|
if (fee.walletStatus && label[fee.walletStatus]) return label[fee.walletStatus]
|
|
}
|
|
const labels: Record<string, string> = {
|
|
FREEZE_PENDING: '冻结处理中', FREEZE_FAILED: '冻结未成功', FROZEN: '等待扣款', SETTLE_PENDING: '扣款处理中', SETTLE_ACTUAL_PENDING: '实际费用结算中',
|
|
SETTLE_FAILED: '扣款未成功', SETTLE_ACTUAL_FAILED: '实际费用未结清', SETTLED: '已扣款', RELEASE_PENDING: '释放处理中', RELEASE_FAILED: '释放未成功',
|
|
RELEASED: '已释放冻结', REFUND_PENDING: '退款处理中', REFUND_FAILED: '退款未成功', REFUNDED: '已退款', ZERO: '已确认零元', NOT_APPLICABLE: '不适用', TERMINATED: '已终止',
|
|
}
|
|
return fee.walletStatus ? labels[fee.walletStatus] || '待核定' : '待核定'
|
|
}
|
|
export function feeTone(fee: ProcurementFee): 'success' | 'warning' | 'danger' | 'info' {
|
|
if (fee.determination === 'REJECTED') return 'danger'
|
|
if (fee.walletStatus === 'NOT_REQUIRED' && fee.determination === 'APPROVED') return 'info'
|
|
if (fee.walletStatus?.endsWith('_FAILED')) return 'danger'
|
|
if (['SETTLED', 'REFUNDED', 'ZERO', 'NOT_APPLICABLE', 'RELEASED'].includes(fee.walletStatus || '')) return 'success'
|
|
if (fee.walletStatus === 'TERMINATED') return 'info'
|
|
return 'warning'
|
|
}
|
|
export function feeTotals(fees: ProcurementFee[]) {
|
|
let paid = 0n, refunded = 0n, estimatedHold = 0n
|
|
for (const fee of fees) {
|
|
const estimate = fee.purpose === 'INTERNATIONAL_ESTIMATE'
|
|
const include = (!estimate && fee.walletStatus === 'SETTLED') || fee.walletStatus === 'REFUNDED'
|
|
|| (estimate && ['FROZEN', 'RELEASE_PENDING', 'RELEASE_FAILED'].includes(fee.walletStatus || ''))
|
|
if (!include) continue
|
|
const amount = cents(fee.amount)
|
|
if (amount === null || amount < 0n || fee.currency !== 'CNY') return null
|
|
if (estimate) estimatedHold += amount
|
|
else if (fee.walletStatus === 'REFUNDED') refunded += amount
|
|
else paid += amount
|
|
}
|
|
return { paid: decimal(paid), refunded: decimal(refunded), net: decimal(paid - refunded), estimatedHold: decimal(estimatedHold) }
|
|
}
|
|
export interface ProcurementDraft {
|
|
procurementDirection?: ProcurementDirection
|
|
purchaseCategory: string
|
|
productName: string
|
|
productSpecification: string
|
|
productUrl: string
|
|
purchaseQuantity: string
|
|
budgetUnitPrice: string
|
|
currency: 'CNY' | 'USD'
|
|
receivingAddress: string
|
|
contactName: string
|
|
contactPhone: string
|
|
remark: string
|
|
}
|
|
export function emptyProcurementDraft(): ProcurementDraft {
|
|
return { procurementDirection: 'OVERSEAS_TO_DOMESTIC', purchaseCategory: '', productName: '', productSpecification: '', productUrl: '', purchaseQuantity: '1', budgetUnitPrice: '', currency: 'USD', receivingAddress: '', contactName: '', contactPhone: '', remark: '' }
|
|
}
|
|
export function draftBudgetTotal(draft: ProcurementDraft) {
|
|
const price = cents(draft.budgetUnitPrice)
|
|
if (price === null || price < 0n || !/^\d+$/.test(draft.purchaseQuantity)) return null
|
|
return decimal(price * BigInt(draft.purchaseQuantity))
|
|
}
|
|
export function buildProcurementCreate(draft: ProcurementDraft, customerName: string, key: string): ProcurementCreate {
|
|
for (const [field, label, max] of [
|
|
['productName', '商品名称', 255], ['receivingAddress', '完整目的地收货地址', 1000],
|
|
['contactName', '最终收件人', 128], ['contactPhone', '联系电话', 64],
|
|
] as const) {
|
|
if (!draft[field].trim() || draft[field].trim().length > max) throw new Error(`请填写有效的${label}`)
|
|
}
|
|
if (draft.purchaseCategory.trim().length > 128) throw new Error('采购品类超过长度限制')
|
|
if (!customerName.trim()) throw new Error('当前企业信息尚未加载,请刷新登录信息后重试')
|
|
if (!/^\d+$/.test(draft.purchaseQuantity) || !Number.isSafeInteger(Number(draft.purchaseQuantity)) || Number(draft.purchaseQuantity) <= 0) throw new Error('采购数量必须为正整数')
|
|
const price = cents(draft.budgetUnitPrice)
|
|
if (price === null || price < 0n) throw new Error('请明确填写非负预算单价,最多两位小数;免费样品填写 0')
|
|
if (price > BigInt(Number.MAX_SAFE_INTEGER) || cents(Number(decimal(price))) !== price) throw new Error('预算金额过大,无法准确表示,请核对金额')
|
|
if (!['CNY', 'USD'].includes(draft.currency)) throw new Error('预算币种只支持 CNY 或 USD')
|
|
if (!/^[+0-9][0-9 ()-]{5,63}$/.test(draft.contactPhone.trim())) throw new Error('请填写快递可联系的电话号码,微信或邮箱不能代替电话')
|
|
if (draft.productUrl && (!productHref(draft.productUrl.trim()) || draft.productUrl.trim().length > 2000)) throw new Error('商品链接须为有效的 http 或 https 地址')
|
|
if (draft.productSpecification.length > 500 || draft.remark.length > 1000) throw new Error('商品规格或备注超过长度限制')
|
|
if (draft.procurementDirection && !['OVERSEAS_TO_DOMESTIC', 'DOMESTIC_TO_OVERSEAS'].includes(draft.procurementDirection)) throw new Error('请选择有效采购方向')
|
|
return { procurementDirection: draft.procurementDirection || 'OVERSEAS_TO_DOMESTIC', customerName: customerName.trim(), purchaseCategory: draft.purchaseCategory.trim(), productName: draft.productName.trim(),
|
|
productSpecification: draft.productSpecification.trim() || undefined, productUrl: draft.productUrl.trim() || undefined,
|
|
purchaseQuantity: Number(draft.purchaseQuantity), budgetUnitPrice: Number(decimal(price)), currency: draft.currency,
|
|
receivingAddress: draft.receivingAddress.trim(), contactName: draft.contactName.trim(), contactPhone: draft.contactPhone.trim(),
|
|
remark: draft.remark.trim() || undefined, idempotencyKey: key }
|
|
}
|
|
|
|
export type ProcurementDirection = 'OVERSEAS_TO_DOMESTIC' | 'DOMESTIC_TO_OVERSEAS'
|
|
export const procurementDirections: Record<ProcurementDirection, string> = {
|
|
OVERSEAS_TO_DOMESTIC: '海外采购,发往国内', DOMESTIC_TO_OVERSEAS: '国内采购,发往海外',
|
|
}
|
|
export function directionLabel(direction?: string | null) { return direction === 'DOMESTIC_TO_OVERSEAS' ? procurementDirections.DOMESTIC_TO_OVERSEAS : procurementDirections.OVERSEAS_TO_DOMESTIC }
|
|
// 两个历史段编码表示起运段和目的地段,展示区域按订单方向转换。
|
|
export function regionText(text: string, direction?: string | null) {
|
|
return direction === 'DOMESTIC_TO_OVERSEAS' ? text.replaceAll('海外', '__ORIGIN__').replaceAll('国内', '海外').replaceAll('__ORIGIN__', '国内') : text
|
|
}
|