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.
chajia/lib/embed/host-bridge-react.tsx

290 lines
7.7 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.

"use client";
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
type ReactNode,
} from "react";
import {
isChajiaModuleId,
listTopLevelKeys,
moduleToHubRoute,
parseChajiaPostMessage,
postToHost,
type ChajiaFillAckPayload,
type ChajiaModuleId,
type ChajiaQuoteResultPayload,
type ChajiaQuoteSavePayload,
type MsGuestFillPayload,
type MsLoggedInFillPayload,
} from "@/lib/embed/host-bridge";
export type HostFillState = {
module: ChajiaModuleId;
requestId?: string;
form: unknown;
seq: number;
};
type HostBridgeContextValue = {
activeModule: ChajiaModuleId | null;
fill: HostFillState | null;
setActiveModule: (m: ChajiaModuleId) => void;
clearFill: () => void;
reportQuoteResult: (payload: ChajiaQuoteResultPayload, requestId?: string) => void;
reportQuoteSave: (payload: ChajiaQuoteSavePayload, requestId?: string) => void;
reportError: (code: string, message: string, module?: ChajiaModuleId) => void;
/** MS_GUEST 表单预填快照 */
msGuestPrefill: MsGuestFillPayload | null;
/** MS_LOGGED_IN 表单预填快照(可晚到 / 可重复) */
msLoggedInPrefill: MsLoggedInFillPayload | null;
};
const HostBridgeContext = createContext<HostBridgeContextValue | null>(null);
export function useHostBridge(): HostBridgeContextValue {
const ctx = useContext(HostBridgeContext);
if (!ctx) {
throw new Error("useHostBridge 须在 HostBridgeProvider 内使用");
}
return ctx;
}
/** 非嵌入场景可空 */
export function useHostBridgeOptional(): HostBridgeContextValue | null {
return useContext(HostBridgeContext);
}
export function HostBridgeProvider({
children,
initialModule = null,
}: {
children: ReactNode;
/** URL module/entry 直进模块 */
initialModule?: ChajiaModuleId | null;
}) {
const [activeModule, setActiveModuleState] = useState<ChajiaModuleId | null>(
initialModule,
);
const [fill, setFill] = useState<HostFillState | null>(null);
const initialAppliedRef = useRef(false);
const setActiveModule = useCallback((m: ChajiaModuleId) => {
setActiveModuleState(m);
postToHost("chajia:module-changed", { module: m, payload: { module: m } });
}, []);
const clearFill = useCallback(() => setFill(null), []);
const reportQuoteResult = useCallback(
(payload: ChajiaQuoteResultPayload, requestId?: string) => {
postToHost("chajia:quote-result", {
request_id: requestId ?? payload.request_id,
module: payload.module,
payload,
});
},
[],
);
const reportQuoteSave = useCallback(
(payload: ChajiaQuoteSavePayload, requestId?: string) => {
postToHost("chajia:quote-save", {
request_id: requestId ?? payload.request_id,
module: payload.module,
payload,
});
},
[],
);
const reportError = useCallback(
(code: string, message: string, module?: ChajiaModuleId) => {
postToHost("chajia:error", {
module,
payload: { code, message, module },
});
},
[],
);
// URL 直进:挂载后通知宿主一次(不重复刷 module-changed)
useEffect(() => {
if (!initialModule || initialAppliedRef.current) return;
initialAppliedRef.current = true;
setActiveModuleState(initialModule);
postToHost("chajia:module-changed", {
module: initialModule,
payload: { module: initialModule, from: "url" },
});
}, [initialModule]);
useEffect(() => {
postToHost("chajia:ready", {
payload: {
modules: [
"MS_GUEST",
"MS_LOGGED_IN",
"FLOCK_GUEST",
"FLOCK_LOGGED_IN",
],
capabilities: [
"select-module",
"fill",
"fill-cache",
"quote-result",
"quote-save",
"scroll-quotes",
"scroll-top",
"url-module",
],
active_module: initialModule,
},
});
const onMessage = (ev: MessageEvent) => {
const msg = parseChajiaPostMessage(ev.data);
if (!msg) return;
if (msg.type === "chajia:ping") {
postToHost("chajia:pong", { request_id: msg.request_id });
return;
}
// 宿主请求滚到承运商报价列表(私卡询价弹窗)
if (msg.type === "chajia:scroll-quotes") {
window.dispatchEvent(new CustomEvent("chajia-host-scroll-quotes"));
return;
}
if (msg.type === "chajia:select-module") {
const m =
msg.module ??
(msg.payload &&
typeof msg.payload === "object" &&
isChajiaModuleId((msg.payload as { module?: unknown }).module)
? (msg.payload as { module: ChajiaModuleId }).module
: null);
if (!m) {
postToHost("chajia:error", {
request_id: msg.request_id,
payload: {
code: "INVALID_MODULE",
message: "select-module 缺少有效 module",
},
});
return;
}
setActiveModule(m);
return;
}
if (msg.type === "chajia:fill") {
const m = msg.module;
if (!m) {
postToHost("chajia:error", {
request_id: msg.request_id,
payload: {
code: "INVALID_MODULE",
message:
"fill 必须带 module(MS_GUEST|MS_LOGGED_IN|FLOCK_GUEST|FLOCK_LOGGED_IN)",
},
});
return;
}
const raw = msg.payload;
const form =
raw &&
typeof raw === "object" &&
"form" in (raw as object) &&
(raw as { form: unknown }).form != null
? (raw as { form: unknown }).form
: raw;
const selectModule =
!(
raw &&
typeof raw === "object" &&
(raw as { select_module?: boolean }).select_module === false
);
if (selectModule) {
setActiveModule(m);
}
const nextSeq = Date.now();
// 始终缓存:表单未挂载也可晚到灌表;可重复 fill(seq 递增)
setFill({
module: m,
requestId: msg.request_id,
form,
seq: nextSeq,
});
const ack: ChajiaFillAckPayload = {
ok: true,
module: m,
applied_keys: listTopLevelKeys(form),
message:
"已缓存预填(可重复)。已切入对应模块;表单挂载后写入搜索框/货物(地址须用户点选联想确认)。",
};
postToHost("chajia:fill-ack", {
request_id: msg.request_id,
module: m,
payload: ack,
});
}
};
window.addEventListener("message", onMessage);
return () => window.removeEventListener("message", onMessage);
}, [setActiveModule, initialModule]);
const msGuestPrefill = useMemo((): MsGuestFillPayload | null => {
if (!fill || fill.module !== "MS_GUEST") return null;
return (fill.form ?? null) as MsGuestFillPayload | null;
}, [fill]);
const msLoggedInPrefill = useMemo((): MsLoggedInFillPayload | null => {
if (!fill || fill.module !== "MS_LOGGED_IN") return null;
return (fill.form ?? null) as MsLoggedInFillPayload | null;
}, [fill]);
const value = useMemo(
() => ({
activeModule,
fill,
setActiveModule,
clearFill,
reportQuoteResult,
reportQuoteSave,
reportError,
msGuestPrefill,
msLoggedInPrefill,
}),
[
activeModule,
fill,
setActiveModule,
clearFill,
reportQuoteResult,
reportQuoteSave,
reportError,
msGuestPrefill,
msLoggedInPrefill,
],
);
return (
<HostBridgeContext.Provider value={value}>
{children}
</HostBridgeContext.Provider>
);
}
export { moduleToHubRoute };