import type { Amount, ProcurementCreate, ProcurementFee } from './api/procurement' export const procurementStatuses: Record = { PENDING_REVIEW: '待审核', WAIT_PURCHASE: '待购买', WAIT_OVERSEAS_RECEIPT: '待海外到仓', OVERSEAS_RECEIVING: '海外收货中', WAIT_PACKING: '待装箱', WAIT_LABELS: '待面单及预估处理', WAIT_OVERSEAS_SHIPMENT: '待海外交寄', INTERNATIONAL_TRANSIT: '国际运输中', DOMESTIC_RECEIVING: '国内收货中', WAIT_CHARGES: '待国内核费', WAIT_CUSTOMER_SHIPMENT: '待寄客户', SHIPPED_TO_CUSTOMER: '已寄客户', REJECTED: '已拒绝', CANCELLED: '已取消', EXCEPTION_CLOSED: '异常关闭', } export const feePurposes: Record = { SAMPLE_PAYMENT: '样品货款', INTERNATIONAL_ESTIMATE: '预估国际运费', INTERNATIONAL_FREIGHT: '实际国际运费', DUTY: '关税', DOMESTIC_FREIGHT: '国内运费', OTHER: '其他费用', ADJUSTMENT: '追加费用', REFUND: '退款', } export function statusLabel(status: string | null | undefined) { return status ? procurementStatuses[status] || '待核对进度' : '—' } export function statusTone(status: string | null | undefined): 'success' | 'warning' | 'danger' | 'info' | 'primary' { if (status === 'SHIPPED_TO_CUSTOMER') return 'success' if (status === 'REJECTED' || status === 'EXCEPTION_CLOSED') return 'danger' if (status === 'CANCELLED') return 'info' if (status === 'INTERNATIONAL_TRANSIT') 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.purpose === 'INTERNATIONAL_ESTIMATE') { const label: Record = { FROZEN: '已冻结,未扣款', ZERO: '零预估已确认', SETTLED: '原预估已结清', RELEASED: '预估已释放' } if (fee.walletStatus && label[fee.walletStatus]) return label[fee.walletStatus] } const labels: Record = { 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.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 { 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 { 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('商品规格或备注超过长度限制') return { 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 } }