/** * HandsontablePro 表头筛选 → 列表 API 入参适配。 * 仅映射现有 DTO 能承接的字段;高级运算优先走 filters[](P1)。 */ import { parseFilterValue, toServerFilterItem, toServerSearchValue, type ServerFilterItem, type ToServerFilterOptions, } from '@/components/HandsontablePro/filterValue'; import { MAX_HEADER_FILTERS, clampBatchValues } from '@/utils/headerFilterLimits'; export type HeaderSearchParams = Record; function asTrimmedString(v: unknown): string { if (v == null) return ''; return String(v).trim(); } /** 取首个非空明文检索值(contains/equals) */ export function pickHeaderText(params: HeaderSearchParams, keys: string[]): string { for (const key of keys) { const plain = toServerSearchValue(asTrimmedString(params[key])); if (plain) return plain; } return ''; } /** 日期列:支持 field / fieldStart+fieldEnd / "start,end" */ export function pickDateRange( params: HeaderSearchParams, field: string, ): { start?: string; end?: string } { const startKey = `${field}Start`; const endKey = `${field}End`; let start = asTrimmedString(params[startKey]); let end = asTrimmedString(params[endKey]); if (!start && !end) { const raw = asTrimmedString(params[field]); if (raw.includes(',')) { const [s, e] = raw.split(','); start = (s || '').trim(); end = (e || '').trim(); } } return { start: start || undefined, end: end || undefined, }; } type FieldMapEntry = string | { field: string; options?: ToServerFilterOptions }; /** 将多列表头编码值转为 filters[](字段名可映射) */ export function collectServerFilters( params: HeaderSearchParams, fieldMap: Record, ): ServerFilterItem[] { const items: ServerFilterItem[] = []; const seen = new Set(); for (const [paramKey, mapEntry] of Object.entries(fieldMap)) { const apiField = typeof mapEntry === 'string' ? mapEntry : mapEntry.field; const options = typeof mapEntry === 'string' ? undefined : mapEntry.options; if (seen.has(apiField) && !asTrimmedString(params[paramKey])) continue; const item = toServerFilterItem(asTrimmedString(params[paramKey]), apiField, options); if (!item) continue; const idx = items.findIndex((x) => x.field === apiField); if (idx >= 0) items.splice(idx, 1); items.push(item); seen.add(apiField); if (items.length >= MAX_HEADER_FILTERS) break; } return items.map((item) => { if (item.values?.length) { const { values } = clampBatchValues(item.values); return { ...item, values, value: values.join(',') }; } return item; }); } const ORDER_NUMBER_FIELD_MAP: Record = { waybillNo: { field: 'serviceNo', options: { preferExact: true, multiAsIn: true } }, serviceNo: { field: 'serviceNo', options: { preferExact: true, multiAsIn: true } }, customerOrderNo: { field: 'clientNo', options: { preferExact: true, multiAsIn: true } }, clientNo: { field: 'clientNo', options: { preferExact: true, multiAsIn: true } }, trackingNo: { field: 'trackNo', options: { preferExact: true, multiAsIn: true } }, trackNo: { field: 'trackNo', options: { preferExact: true, multiAsIn: true } }, downstreamServiceNo: { field: 'downstreamServiceNo', options: { preferExact: true, multiAsIn: true }, }, }; const ORDER_EXTRA_FIELD_MAP: Record = { orderStatus: { field: 'orderStatus', options: { multiAsIn: true } }, fulfillmentType: { field: 'fulfillmentType', options: { multiAsIn: true } }, shippingMethod: 'productCode', serviceChannel: 'channelCode', isPaid: { field: 'isPaid', options: { multiAsIn: true } }, receivableAmount: { field: 'payableAmount', options: { asNumber: true } }, payableAmount: { field: 'payableAmount', options: { asNumber: true } }, predeclareFailReason: 'predeclareFailReason', allocationPendingReason: 'allocationPendingReason', }; export interface OrderListHeaderQuery { keyword?: string; createTimeStart?: string; createTimeEnd?: string; filters: ServerFilterItem[]; } /** * 订单列表表头 → API。 * - 单号/状态/金额等走 filters[];无字段级筛选时单号回落 keyword * - 创建时间走 createTimeStart/End(与工具栏合并由调用方决定) */ export function adaptOrderListHeaderFilters( params: HeaderSearchParams, ): OrderListHeaderQuery { const numberFilters = collectServerFilters(params, ORDER_NUMBER_FIELD_MAP); const extraFilters = collectServerFilters(params, ORDER_EXTRA_FIELD_MAP); const filters = [...numberFilters, ...extraFilters]; // 日期:优先双写 Start/End;若仅有单边比较编码则进 filters const { start, end } = pickDateRange(params, 'createTime'); for (const dateKey of ['payTime', 'shipTime', 'receiveTime', 'signTime'] as const) { const range = pickDateRange(params, dateKey); if (range.start || range.end) { if (range.start && range.end) { filters.push({ field: dateKey, op: 'between', value: range.start, value2: range.end, }); } else if (range.start) { filters.push({ field: dateKey, op: 'gte', value: range.start }); } else if (range.end) { filters.push({ field: dateKey, op: 'lte', value: range.end }); } } else { const item = toServerFilterItem(asTrimmedString(params[dateKey]), dateKey); if (item) filters.push(item); } } // 无字段级单号筛选时,用任一单号列明文作为 keyword(兼容旧后端;精确查询优先走 filters) let keyword: string | undefined; if (!numberFilters.length) { const text = pickHeaderText(params, Object.keys(ORDER_NUMBER_FIELD_MAP)); keyword = text || undefined; } else { // 已有精确 filters 时不再附带模糊 keyword,避免 AND 冲突 keyword = undefined; } return { keyword, createTimeStart: start, createTimeEnd: end, filters, }; } export interface CustomerListHeaderQuery { keyword?: string; salesman?: string; customerService?: string; status?: number; } /** 客户列表表头 → 现有 CustomerPaginatedInput */ export function adaptCustomerListHeaderFilters( params: HeaderSearchParams, ): CustomerListHeaderQuery { const keyword = pickHeaderText(params, [ 'customerCode', 'customerName', 'email', 'phone', 'wechatNickname', ]) || undefined; const salesman = pickHeaderText(params, ['salesPerson', 'salesman']) || undefined; const customerService = pickHeaderText(params, ['customerService']) || undefined; let status: number | undefined; const statusRaw = asTrimmedString(params.status); if (statusRaw) { const { op, value } = parseFilterValue(statusRaw); if (op === 'contains' || op === 'equals') { const first = value.split(',')[0]?.trim() || ''; const n = Number(first); if (!Number.isNaN(n)) status = n; } } return { keyword, salesman, customerService, status }; } export interface AccountFlowHeaderQuery { keyword?: string; bizType?: string; createTimeStart?: string; createTimeEnd?: string; amountMin?: number; amountMax?: number; } /** 账户流水表头 → AmountMin/Max + 时间 */ export function adaptAccountFlowHeaderFilters( params: HeaderSearchParams, ): AccountFlowHeaderQuery { const keyword = pickHeaderText(params, ['serialNumber', 'bizNo', 'remark']) || undefined; const bizType = pickHeaderText(params, ['bizType', 'bizTypeName']) || undefined; const { start, end } = pickDateRange(params, 'createTime'); let amountMin: number | undefined; let amountMax: number | undefined; const amountRaw = asTrimmedString(params.amount); if (amountRaw) { const { op, value } = parseFilterValue(amountRaw); const n = Number(value); if (!Number.isNaN(n) && value !== '') { if (op === 'gte' || op === 'gt') amountMin = n; else if (op === 'lte' || op === 'lt') amountMax = n; else if (op === 'equals' || op === 'contains') { amountMin = n; amountMax = n; } } } return { keyword, bizType, createTimeStart: start, createTimeEnd: end, amountMin, amountMax, }; } /** 仓储 keyword 列表:SKU/包裹等 */ export function adaptKeywordHeaderFilters( params: HeaderSearchParams, keys: string[], ): string | undefined { return pickHeaderText(params, keys) || undefined; } /** 轨迹列表:补齐 createTime 区间 */ export function adaptTrackOrderDateRange( params: HeaderSearchParams, fallback?: { createTimeStart?: string; createTimeEnd?: string }, ): { createTimeStart?: string; createTimeEnd?: string } { const { start, end } = pickDateRange(params, 'createTime'); return { createTimeStart: start || fallback?.createTimeStart || undefined, createTimeEnd: end || fallback?.createTimeEnd || undefined, }; } /** 给列定义打上服务端筛选标记 */ export function withServerFilterColumns( columns: T[], serverKeys: Iterable, extras?: Partial | ((col: T) => Partial), ): T[] { const set = new Set(serverKeys); return columns.map((col) => { if (!set.has(col.key)) return col; const extra = typeof extras === 'function' ? extras(col) : extras || {}; return { ...col, serverFilter: true, ...extra }; }); } /** 可排序列标记(remote sort) */ export function withSortableColumns( columns: T[], sortableKeys: Iterable, ): T[] { const set = new Set(sortableKeys); return columns.map((col) => (set.has(col.key) ? { ...col, sortable: true } : col)); } export interface ReceivablePayableHeaderQuery { keyword?: string; feeType?: string; costItem?: string; chargingStage?: string; customerCode?: string; isPaid?: number; } const RECEIVABLE_PAYABLE_STAGE_LABEL_MAP: Record = { 预报: 'forecast', 入库: 'inbound', 出库: 'outbound', }; /** 应收应付表头 → 列表 API */ export function adaptReceivablePayableHeaderFilters( params: HeaderSearchParams, ): ReceivablePayableHeaderQuery { const keyword = pickHeaderText(params, ['serviceNo', 'clientNo', 'trackNo']) || undefined; let feeType = pickHeaderText(params, ['feeType', 'feeTypeName']) || undefined; if (feeType === '应收') feeType = 'receivable'; if (feeType === '应付') feeType = 'payable'; let chargingStage = pickHeaderText(params, ['chargingStage', 'chargingStageName']) || undefined; if (chargingStage && RECEIVABLE_PAYABLE_STAGE_LABEL_MAP[chargingStage]) { chargingStage = RECEIVABLE_PAYABLE_STAGE_LABEL_MAP[chargingStage]; } const costItem = pickHeaderText(params, ['costItem']) || undefined; const customerCode = pickHeaderText(params, ['customerCode']) || undefined; const isPaid = parseReceivablePayableIsPaid(params); return { keyword, feeType: feeType && feeType !== 'all' ? feeType : undefined, costItem: costItem && costItem !== 'all' ? costItem : undefined, chargingStage: chargingStage && chargingStage !== 'all' ? chargingStage : undefined, customerCode, isPaid, }; } function parseReceivablePayableIsPaid(params: HeaderSearchParams): number | undefined { const raw = pickHeaderText(params, ['isPaid']); if (!raw) return undefined; if (raw === '1' || raw === '已支付') return 1; if (raw === '0' || raw === '未支付') return 0; const first = raw.split(',')[0]?.trim() ?? ''; if (first === '1' || first === '已支付') return 1; if (first === '0' || first === '未支付') return 0; const n = Number(first); if (n === 0 || n === 1) return n; return undefined; }