You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

273 lines
8.0 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* MotherShip 官网 Widget 档位注册表
*
* 原则:官网/API 返回什么档位,宿主就展示什么——展示层不隐藏、不合成新档位。
* 本文件仅负责中文标签、Tab 排序、去重、service_level 规范化。
*/
export type QuoteTierKey = {
service_level: string;
rate_option: string;
carrier?: 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);
const carrier = (q.carrier ?? "").trim().toLowerCase();
const key = carrier
? `${level}/${q.rate_option}/${carrier}`
: `${level}/${q.rate_option}`;
deduped.set(key, { ...q, service_level: level });
}
return [...deduped.values()].sort((a, b) => {
const byTier = compareMotherShipUiTierSortOrder(
a.service_level,
a.rate_option,
b.service_level,
b.rate_option,
);
if (byTier !== 0) return byTier;
return (a.carrier ?? "").localeCompare(b.carrier ?? "");
});
}
/** 从报价列表推导应展示的 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;
}