front/src/utils/headerFilterAdapter.ts
zzh 70d59a0ea9 feat: 接入 IDE 官网与地区地图,并同步运营/财务/订单前端改动
新增 IDE 官网静态资源与 D3 分区域地图布局,构建时复制至 dist/site;同步应收应付、代发打包、订单详情等页面优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-24 19:24:05 +08:00

349 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<string, unknown>;
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<string, FieldMapEntry>,
): ServerFilterItem[] {
const items: ServerFilterItem[] = [];
const seen = new Set<string>();
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<string, FieldMapEntry> = {
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<string, FieldMapEntry> = {
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<T extends { key: string }>(
columns: T[],
serverKeys: Iterable<string>,
extras?: Partial<T> | ((col: T) => Partial<T>),
): 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<T extends { key: string }>(
columns: T[],
sortableKeys: Iterable<string>,
): 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<string, string> = {
预报: '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;
}