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.

319 lines
9.1 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.

/**
* 宿主 iframe ↔ 查价页 postMessage 协议(v1)
* 文档:api调用文档/嵌入-postMessage对接文档.md
*/
export const CHAJIA_PM_SOURCE = "chajia" as const;
export const CHAJIA_PM_VERSION = 1 as const;
/** 四条互斥查价模块(与 OpenAPI 路线 ID 一致) */
export type ChajiaModuleId =
| "MS_GUEST"
| "MS_LOGGED_IN"
| "FLOCK_GUEST"
| "FLOCK_LOGGED_IN";
export type ChajiaHostToIframeType =
| "chajia:select-module"
| "chajia:fill"
| "chajia:ping"
/** 宿主请求 iframe 内滚到「选择承运商」报价区 */
| "chajia:scroll-quotes";
export type ChajiaIframeToHostType =
| "chajia:ready"
| "chajia:pong"
| "chajia:module-changed"
| "chajia:fill-ack"
| "chajia:quote-result"
| "chajia:quote-save"
/** 进入二级详情等场景:请求宿主把查价区滚到视口顶部 */
| "chajia:scroll-top"
| "chajia:error";
export type ChajiaPmType = ChajiaHostToIframeType | ChajiaIframeToHostType;
export interface ChajiaPmEnvelope<T extends ChajiaPmType = ChajiaPmType> {
source: typeof CHAJIA_PM_SOURCE;
version: typeof CHAJIA_PM_VERSION;
type: T;
/** 宿主生成,用于配对 fill / 结果 */
request_id?: string;
module?: ChajiaModuleId;
payload?: unknown;
}
/** MotherShip 免账号:表单草稿(与嵌入 QuoteForm 一致) */
export interface MsGuestFillPayload {
business_customer_id?: string;
/** ccnew LR_Base_User.F_Account */
business_user_account?: string;
pickup?: { street?: string; city?: string; state?: string; zip?: string };
delivery?: { street?: string; city?: string; state?: string; zip?: string };
weight?: { value: number; unit?: "lb" | "kg" | "t" };
dimensions?: {
length: number;
width: number;
height: number;
unit?: "in" | "cm" | "m";
};
pallet_count?: number;
cargo_type?: string;
service_level?: "standard" | "guaranteed";
display_unit?: "imperial" | "metric";
}
/**
* MotherShip 登录后地址预填。
* 宿主可只传整段文本(street / formatted_address),写入搜索框;
* place_id / mothership_option_id 可选——无则用户须点选联想确认。
*/
export interface MsLoggedInAddressFill {
street?: string;
city?: string;
state?: string;
zip?: string;
place_id?: string;
formatted_address?: string;
mothership_option_id?: string;
mothership_display_label?: string;
}
/** MotherShip 登录后:地址文本 + 附加服务 + 货物行 */
export interface MsLoggedInFillPayload {
business_customer_id?: string;
/** ccnew LR_Base_User.F_Account;无 BC 时反查加价客户 */
business_user_account?: string;
pickup_address?: MsLoggedInAddressFill;
delivery_address?: MsLoggedInAddressFill;
ready_date?: string;
ready_time?: string;
pickup_accessorials?: string[];
delivery_accessorials?: string[];
cargo_lines?: Array<{
cargo_type?: string;
quantity?: number;
weight_lb?: number;
length_in?: number;
width_in?: number;
height_in?: number;
}>;
}
/** 从地址 fill 抽出搜索框文案(整段文本优先) */
export function msLoggedInAddressQueryText(
addr: MsLoggedInAddressFill | null | undefined,
): string {
if (!addr) return "";
const formatted = addr.formatted_address?.trim() ?? "";
if (formatted) return formatted;
const street = addr.street?.trim() ?? "";
if (street) return street;
const parts = [addr.city, addr.state, addr.zip]
.map((x) => (x ?? "").trim())
.filter(Boolean);
return parts.join(", ");
}
/** Flock 免账号 */
export interface FlockGuestFillPayload {
pickup_date: string;
pickup_zip: string;
delivery_zip: string;
pallet_count: number;
total_weight: { value: number; unit?: "lb" | "kg" | "t" };
dimensions: {
length: number;
width: number;
height: number;
unit?: "in" | "cm" | "m";
};
}
/** Flock 登录后 Quick(核心字段) */
export interface FlockLoggedInFillPayload {
pickup_date: string;
pickup_zip: string;
delivery_zip: string;
pickup_type?: string;
delivery_type?: string;
items: Array<{
quantity: number;
packaging_type?: string;
length_in: number;
width_in: number;
height_in: number;
total_weight_lb: number;
freight_class?: string;
description?: string;
}>;
pickup_liftgate?: boolean;
delivery_liftgate?: boolean;
additional_services?: string[];
vehicle_types?: string[];
}
export type ChajiaFillPayloadByModule = {
MS_GUEST: MsGuestFillPayload;
MS_LOGGED_IN: MsLoggedInFillPayload;
FLOCK_GUEST: FlockGuestFillPayload;
FLOCK_LOGGED_IN: FlockLoggedInFillPayload;
};
export interface ChajiaFillMessagePayload<
M extends ChajiaModuleId = ChajiaModuleId,
> {
/** 是否切到对应模块(默认 true) */
select_module?: boolean;
/** 模块表单数据 */
form: ChajiaFillPayloadByModule[M];
}
/** 查价成功后回传(与程序 QuoteItem / QuoteDetail 对齐) */
export interface ChajiaQuoteResultPayload {
quote_id: string;
request_id: string;
status: "done" | "failed" | "expired";
module: ChajiaModuleId;
currency: string;
source_type?: string | null;
is_realtime?: boolean;
quotes: Array<{
carrier: string;
service_level: string;
rate_option: string;
transit_days: string;
transit_description: string;
raw_freight: number;
surcharges: number;
raw_total: number;
markup_percent: number;
markup_amount: number;
final_total: number;
/** 给宿主的显式别名:原价(未加价) */
base_total_before_markup?: number;
/** 给宿主的显式别名:本客户加价金额 */
customer_markup_amount?: number;
/** 给宿主的显式别名:本客户最终展示价 */
customer_final_total?: number;
/** 宿主无需再猜:true 表示 final_total 已是客户价 */
is_customer_markup_applied?: boolean;
/** 宿主可直接据此取展示价 */
pricing_mode?: "customer_final";
breakdown: Array<{ item: string; amount: number }>;
/** 宿主保存快照时标记选中行 */
selected?: boolean;
guaranteed?: boolean;
guarantee_amount?: number | null;
estimated_pickup?: string | null;
estimated_delivery?: string | null;
}>;
error_code?: string | null;
error_message?: string | null;
}
/** 用户点击「保存本次询价记录」回传宿主(含选中报价) */
export interface ChajiaQuoteSavePayload extends ChajiaQuoteResultPayload {
action: "save";
selected_carrier?: string | null;
coverage?: "basic" | "freight_protect" | null;
cargo_value_usd?: number | null;
ready_date?: string | null;
estimated_pickup?: string | null;
pickup_address?: MsLoggedInAddressFill;
delivery_address?: MsLoggedInAddressFill;
cargo_lines?: Array<{
cargo_type?: string;
quantity?: number;
weight_lb?: number;
length_in?: number;
width_in?: number;
height_in?: number;
}>;
}
export interface ChajiaErrorPayload {
code: string;
message: string;
module?: ChajiaModuleId;
}
export interface ChajiaFillAckPayload {
ok: boolean;
module: ChajiaModuleId;
applied_keys: string[];
message?: string;
}
const MODULE_SET = new Set<string>([
"MS_GUEST",
"MS_LOGGED_IN",
"FLOCK_GUEST",
"FLOCK_LOGGED_IN",
]);
export function isChajiaModuleId(v: unknown): v is ChajiaModuleId {
return typeof v === "string" && MODULE_SET.has(v);
}
export function moduleToHubRoute(
module: ChajiaModuleId,
): { provider: "mothership" | "flock"; mode: "anon" | "login" } {
switch (module) {
case "MS_GUEST":
return { provider: "mothership", mode: "anon" };
case "MS_LOGGED_IN":
return { provider: "mothership", mode: "login" };
case "FLOCK_GUEST":
return { provider: "flock", mode: "anon" };
case "FLOCK_LOGGED_IN":
return { provider: "flock", mode: "login" };
}
}
export function parseChajiaPostMessage(
data: unknown,
): ChajiaPmEnvelope | null {
if (!data || typeof data !== "object") return null;
const o = data as Record<string, unknown>;
if (o.source !== CHAJIA_PM_SOURCE) return null;
if (o.version !== CHAJIA_PM_VERSION) return null;
if (typeof o.type !== "string" || !o.type.startsWith("chajia:")) return null;
return {
source: CHAJIA_PM_SOURCE,
version: CHAJIA_PM_VERSION,
type: o.type as ChajiaPmType,
request_id: typeof o.request_id === "string" ? o.request_id : undefined,
module: isChajiaModuleId(o.module) ? o.module : undefined,
payload: o.payload,
};
}
/** iframe → 宿主(仅当嵌入在 iframe 中时发送) */
export function postToHost(
type: ChajiaIframeToHostType,
opts?: {
request_id?: string;
module?: ChajiaModuleId;
payload?: unknown;
targetOrigin?: string;
},
): void {
if (typeof window === "undefined") return;
if (window.parent === window) return;
const msg: ChajiaPmEnvelope = {
source: CHAJIA_PM_SOURCE,
version: CHAJIA_PM_VERSION,
type,
request_id: opts?.request_id,
module: opts?.module,
payload: opts?.payload,
};
window.parent.postMessage(msg, opts?.targetOrigin ?? "*");
}
export function listTopLevelKeys(obj: unknown): string[] {
if (!obj || typeof obj !== "object" || Array.isArray(obj)) return [];
return Object.keys(obj as Record<string, unknown>);
}