|
|
/**
|
|
|
* 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<string, string> = {
|
|
|
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<string, string> = {
|
|
|
lowest: "lowest",
|
|
|
cheapest: "lowest",
|
|
|
fastest: "fastest",
|
|
|
expedited: "fastest",
|
|
|
bestvalue: "bestValue",
|
|
|
最低价格: "lowest",
|
|
|
最快递送: "fastest",
|
|
|
最优性价比: "bestValue",
|
|
|
};
|
|
|
|
|
|
const RATE_OPTION_LABEL_ZH: Record<string, string> = {
|
|
|
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<T extends QuoteTierKey>(
|
|
|
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<string, T>();
|
|
|
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<T extends QuoteTierKey>(
|
|
|
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<T extends QuoteTierKey>(
|
|
|
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<T extends QuoteTierKey>(
|
|
|
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, unknown>,
|
|
|
): 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;
|
|
|
}
|