|
|
/**
|
|
|
* 宿主 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>);
|
|
|
}
|