/** * MotherShip 官网 Widget 档位注册表 * * 原则:官网/API 返回什么档位,宿主就展示什么——展示层不隐藏、不合成新档位。 * 本文件仅负责:中文标签、Tab 排序、去重、service_level 规范化。 */ export type QuoteTierKey = { service_level: string; rate_option: string; }; export type MotherShipUiTabConfig = { /** axel rates 顶层键,如 standard / guaranteed / dedicated */ serviceLevel: string; labelZh: string; sortOrder: number; /** * 该 Tab 下子选项的官网展示顺序(仅排序;不用于过滤 API 档位)。 */ visibleRateOptions: readonly string[]; /** @deprecated 不再用于过滤或顶替展示 */ preferBestValueOverLowest?: boolean; }; /** 已知 Tab;未来 MotherShip 新增 Tab 时在此追加 */ export const MOTHERSHIP_UI_TAB_REGISTRY: readonly MotherShipUiTabConfig[] = [ { serviceLevel: "standard", labelZh: "标准", sortOrder: 0, visibleRateOptions: ["lowest", "fastest", "bestValue"], preferBestValueOverLowest: true, }, { serviceLevel: "guaranteed", labelZh: "保证送达", sortOrder: 1, visibleRateOptions: ["lowest", "fastest", "bestValue"], }, { serviceLevel: "dedicated", labelZh: "专属卡车", sortOrder: 2, visibleRateOptions: ["bestValue", "lowest", "fastest"], }, ] as const; const REGISTRY_BY_LEVEL = new Map( MOTHERSHIP_UI_TAB_REGISTRY.map((t) => [t.serviceLevel, t]), ); const SERVICE_LEVEL_ALIASES: Record = { standard: "standard", guaranteed: "guaranteed", guarantee: "guaranteed", dedicated: "dedicated", dedicatedtruck: "dedicated", truckload: "dedicated", fulltruckload: "dedicated", ftl: "dedicated", 标准: "standard", 保证送达: "guaranteed", 专属卡车: "dedicated", 专属货车: "dedicated", }; const RATE_OPTION_ALIASES: Record = { lowest: "lowest", cheapest: "lowest", fastest: "fastest", expedited: "fastest", bestvalue: "bestValue", 最低价格: "lowest", 最快递送: "fastest", 最优性价比: "bestValue", }; const RATE_OPTION_LABEL_ZH: Record = { lowest: "最低价格", fastest: "最快递送", bestValue: "最优性价比", }; const RATE_OPTION_SORT = new Map( ["lowest", "bestValue", "fastest"].map((k, i) => [k, i]), ); /** axel rates 顶层键 → 规范 service_level(未知键原样保留,便于前向兼容) */ export function normalizeMotherShipServiceLevel(raw: string): string { const key = raw.replace(/[^a-z0-9\u4e00-\u9fff]/gi, "").toLowerCase(); return SERVICE_LEVEL_ALIASES[key] ?? raw.trim().toLowerCase(); } /** 子键 / serviceType → 规范 rate_option */ export function normalizeMotherShipRateOption(raw: string): string | null { const key = raw.replace(/[^a-z0-9\u4e00-\u9fff]/gi, "").toLowerCase(); return RATE_OPTION_ALIASES[key] ?? null; } /** 官网 Tab 中文名;未知档位用友好 fallback */ export function getMotherShipServiceLevelLabel(serviceLevel: string): string { const normalized = normalizeMotherShipServiceLevel(serviceLevel); const known = REGISTRY_BY_LEVEL.get(normalized); if (known) { return known.labelZh; } if (!normalized) { return "其它运价"; } return normalized.charAt(0).toUpperCase() + normalized.slice(1); } /** 官网子选项中文名 */ export function getMotherShipRateOptionLabel( serviceLevel: string, rateOption: string, ): string { return RATE_OPTION_LABEL_ZH[rateOption] ?? rateOption; } export function getMotherShipUiTabConfig( serviceLevel: string, ): MotherShipUiTabConfig | undefined { return REGISTRY_BY_LEVEL.get(normalizeMotherShipServiceLevel(serviceLevel)); } function compareRateOption(a: string, b: string): number { return ( (RATE_OPTION_SORT.get(a) ?? 99) - (RATE_OPTION_SORT.get(b) ?? 99) ); } export function compareMotherShipUiTierSortOrder( levelA: string, rateA: string, levelB: string, rateB: string, ): number { const tabA = getMotherShipUiTabConfig(levelA); const tabB = getMotherShipUiTabConfig(levelB); const orderA = tabA?.sortOrder ?? 100 + levelA.charCodeAt(0); const orderB = tabB?.sortOrder ?? 100 + levelB.charCodeAt(0); if (orderA !== orderB) { return orderA - orderB; } const visA = tabA?.visibleRateOptions ?? []; const visB = tabB?.visibleRateOptions ?? []; const idxA = visA.indexOf(rateA); const idxB = visB.indexOf(rateB); if (idxA >= 0 || idxB >= 0) { return (idxA >= 0 ? idxA : 99) - (idxB >= 0 ? idxB : 99); } return compareRateOption(rateA, rateB); } function sortRateOptionsForTab( serviceLevel: string, options: string[], ): string[] { const tab = REGISTRY_BY_LEVEL.get(normalizeMotherShipServiceLevel(serviceLevel)); if (!tab) { return [...options].sort(compareRateOption); } const order = new Map(tab.visibleRateOptions.map((ro, i) => [ro, i])); return [...options].sort( (a, b) => (order.get(a) ?? 99) - (order.get(b) ?? 99) || compareRateOption(a, b), ); } /** * 展示用报价列表:去重 + 规范化 service_level + 按官网 Tab 顺序排序(不删档、不补档) */ export function filterQuotesForMotherShipUiDisplay( quotes: T[], ): T[] { const structured = quotes.filter((q) => q.service_level && q.rate_option); if (structured.length === 0 && quotes.length > 0) { return quotes; } const deduped = new Map(); for (const q of structured) { const level = normalizeMotherShipServiceLevel(q.service_level); deduped.set(`${level}/${q.rate_option}`, { ...q, service_level: level }); } return [...deduped.values()].sort((a, b) => compareMotherShipUiTierSortOrder( a.service_level, a.rate_option, b.service_level, b.rate_option, ), ); } /** 从报价列表推导应展示的 Tab(按注册表顺序,含未来未知 Tab) */ export function listMotherShipUiTabsFromQuotes( quotes: T[], ): string[] { const levels = new Set( quotes.map((q) => normalizeMotherShipServiceLevel(q.service_level)), ); const ordered: string[] = []; for (const tab of MOTHERSHIP_UI_TAB_REGISTRY) { if (levels.has(tab.serviceLevel)) { ordered.push(tab.serviceLevel); levels.delete(tab.serviceLevel); } } for (const rest of [...levels].sort()) { ordered.push(rest); } return ordered; } /** 某 Tab 下实际存在的 rate_option(按官网顺序排列,不过滤) */ export function listVisibleRateOptionsForTab( quotes: T[], serviceLevel: string, ): string[] { const level = normalizeMotherShipServiceLevel(serviceLevel); const present = [ ...new Set( quotes .filter((q) => normalizeMotherShipServiceLevel(q.service_level) === level) .map((q) => q.rate_option), ), ]; return sortRateOptionsForTab(level, present); } /** 某 Tab 下默认选中的 rate_option(与官网默认「最低价格」一致) */ export function resolveDefaultRateOptionForTab( quotes: T[], serviceLevel: string, ): string { const options = listVisibleRateOptionsForTab(quotes, serviceLevel); if (options.includes("lowest")) { return "lowest"; } if (options.includes("bestValue")) { return "bestValue"; } return options[0] ?? "lowest"; } /** axel rates 顶层键中识别 service level(跳过 fastest 顶层槽) */ export function listServiceLevelsFromRatesBlock( rates: Record, ): string[] { const skip = new Set(["fastest"]); const levels: string[] = []; for (const key of Object.keys(rates)) { if (skip.has(key)) { continue; } const normalized = normalizeMotherShipServiceLevel(key); if (normalized && !levels.includes(normalized)) { levels.push(normalized); } } return levels; }