This commit is contained in:
mlogclub
2026-04-09 10:01:23 +08:00
commit efe801b8bf
707 changed files with 110595 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
export type WidgetHostConfig = {
channelId: string;
baseUrl: string;
apiBaseUrl?: string;
/** 与后端 enums.ExternalSource 一致,默认 web_chat */
externalSource?: string;
title?: string;
subtitle?: string;
position?: "left" | "right";
themeColor?: string;
width?: string;
/** 访客展示名,随请求以 X-External-Name / WS query externalName 传给后端作 ExternalName */
subject?: string;
};
declare global {
interface Window {
CSAgentConfig?: WidgetHostConfig;
__CS_AGENT_WIDGET_CONFIG__?: WidgetHostConfig;
}
}
export function readWidgetConfig(): WidgetHostConfig {
if (typeof window === "undefined") {
return {
channelId: "",
baseUrl: "",
apiBaseUrl: "",
};
}
const query = new URLSearchParams(window.location.search);
const fallback = {
channelId: query.get("channelId") ?? "",
baseUrl: query.get("baseUrl") ?? "",
apiBaseUrl: query.get("apiBaseUrl") ?? undefined,
externalSource: query.get("externalSource") ?? undefined,
title: query.get("title") ?? undefined,
subtitle: query.get("subtitle") ?? undefined,
position: (query.get("position") as "left" | "right" | null) ?? undefined,
themeColor: query.get("themeColor") ?? undefined,
width: query.get("width") ?? undefined,
subject: query.get("subject") ?? undefined,
};
return window.__CS_AGENT_WIDGET_CONFIG__ ?? window.CSAgentConfig ?? fallback;
}
export function setWidgetConfig(config: WidgetHostConfig) {
if (typeof window === "undefined") {
return;
}
window.__CS_AGENT_WIDGET_CONFIG__ = config;
}
+85
View File
@@ -0,0 +1,85 @@
import { setWidgetConfig, type WidgetHostConfig } from "@/lib/widget/config";
const INIT_MESSAGE_TYPE = "cs-agent:init";
const OPEN_MESSAGE_TYPE = "cs-agent:open";
const MINIMIZE_MESSAGE_TYPE = "cs-agent:minimize";
const MAXIMIZED_MESSAGE_TYPE = "cs-agent:maximized";
const READY_MESSAGE_TYPE = "cs-agent:ready";
const REQUEST_MINIMIZE_MESSAGE_TYPE = "cs-agent:request-minimize";
const REQUEST_CLOSE_MESSAGE_TYPE = "cs-agent:request-close";
const REQUEST_TOGGLE_MAXIMIZE_MESSAGE_TYPE = "cs-agent:request-toggle-maximize";
type HostBridgeOptions = {
onOpen?: () => void;
onMinimize?: () => void;
onMaximizedChange?: (isMaximized: boolean) => void;
};
export function bindHostBridge(options: HostBridgeOptions = {}) {
if (typeof window === "undefined") {
return () => undefined;
}
if (window.parent && window.parent !== window) {
window.parent.postMessage({ type: READY_MESSAGE_TYPE }, "*");
}
const handleMessage = (
event: MessageEvent,
) => {
const data = event.data as
| {
type?: string;
payload?: WidgetHostConfig | { isMaximized?: boolean };
}
| undefined;
if (!data?.type) {
return;
}
if (data.type === INIT_MESSAGE_TYPE && data.payload) {
setWidgetConfig(data.payload as WidgetHostConfig);
return;
}
if (data.type === OPEN_MESSAGE_TYPE) {
options.onOpen?.();
return;
}
if (data.type === MINIMIZE_MESSAGE_TYPE) {
options.onMinimize?.();
return;
}
if (data.type === MAXIMIZED_MESSAGE_TYPE) {
options.onMaximizedChange?.(
Boolean((data.payload as { isMaximized?: boolean } | undefined)?.isMaximized),
);
}
};
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}
function postToParent(type: string) {
if (typeof window === "undefined") {
return;
}
if (window.parent && window.parent !== window) {
window.parent.postMessage({ type }, "*");
}
}
export function requestHostMinimize() {
postToParent(REQUEST_MINIMIZE_MESSAGE_TYPE);
}
export function requestHostClose() {
postToParent(REQUEST_CLOSE_MESSAGE_TYPE);
}
export function requestHostToggleMaximize() {
postToParent(REQUEST_TOGGLE_MAXIMIZE_MESSAGE_TYPE);
}
+270
View File
@@ -0,0 +1,270 @@
(function () {
if (window.__CS_AGENT_WIDGET_LOADED__) {
return;
}
window.__CS_AGENT_WIDGET_LOADED__ = true;
var config = window.CSAgentConfig || {};
var baseUrl = String(config.baseUrl || "").replace(/\/$/, "");
if (!config.channelId || !baseUrl) {
console.error("[cs-agent-widget] channelId and baseUrl are required");
return;
}
function resolveWidgetBaseUrl() {
var currentScript = document.currentScript;
if (currentScript && currentScript.src) {
return currentScript.src.replace(/\/sdk\/cs-agent-widget\.js(?:\?.*)?$/, "");
}
if (/\/widget$/.test(baseUrl)) {
return baseUrl;
}
return baseUrl + "/widget";
}
var button = document.createElement("button");
button.type = "button";
button.dataset.csAgentWidget = "launcher";
button.setAttribute("aria-label", config.title || "在线客服");
button.textContent = config.title || "在线客服";
button.style.position = "fixed";
button.style.bottom = "24px";
button.style.right = config.position === "left" ? "" : "24px";
button.style.left = config.position === "left" ? "24px" : "";
button.style.zIndex = "2147483000";
button.style.border = "0";
button.style.borderRadius = "999px";
button.style.padding = "14px 18px";
button.style.background = config.themeColor || "#0f6cbd";
button.style.color = "#fff";
button.style.font = "600 14px/1 sans-serif";
button.style.boxShadow = "0 18px 40px rgba(15, 35, 65, 0.24)";
button.style.cursor = "pointer";
var widgetBaseUrl = resolveWidgetBaseUrl();
var frameUrl = new URL(widgetBaseUrl + "/frame/");
frameUrl.searchParams.set("channelId", config.channelId);
frameUrl.searchParams.set("baseUrl", baseUrl);
if (config.apiBaseUrl) frameUrl.searchParams.set("apiBaseUrl", config.apiBaseUrl);
if (config.title) frameUrl.searchParams.set("title", config.title);
if (config.subtitle) frameUrl.searchParams.set("subtitle", config.subtitle);
if (config.position) frameUrl.searchParams.set("position", config.position);
if (config.themeColor) frameUrl.searchParams.set("themeColor", config.themeColor);
if (config.width) frameUrl.searchParams.set("width", config.width);
var frame = null;
var frameLoaded = false;
var frameReady = false;
var initSent = false;
var isOpen = false;
var isMaximized = false;
var frameHideTimer = null;
var frameDestroyTimer = null;
var animationDuration = 260;
function clearFrameTimers() {
if (frameHideTimer) {
window.clearTimeout(frameHideTimer);
frameHideTimer = null;
}
if (frameDestroyTimer) {
window.clearTimeout(frameDestroyTimer);
frameDestroyTimer = null;
}
}
function applyFrameLayout() {
if (!frame) {
return;
}
frame.style.position = "fixed";
frame.style.border = "0";
frame.style.overflow = "hidden";
frame.style.background = "#fff";
frame.style.zIndex = "2147483000";
frame.style.boxShadow = "0 28px 80px rgba(15, 35, 65, 0.28)";
frame.style.willChange = "top,right,bottom,left,width,height,opacity,transform,border-radius";
frame.style.transition =
"top 260ms cubic-bezier(0.22, 1, 0.36, 1), right 260ms cubic-bezier(0.22, 1, 0.36, 1), bottom 260ms cubic-bezier(0.22, 1, 0.36, 1), left 260ms cubic-bezier(0.22, 1, 0.36, 1), width 260ms cubic-bezier(0.22, 1, 0.36, 1), height 260ms cubic-bezier(0.22, 1, 0.36, 1), opacity 220ms ease, transform 260ms cubic-bezier(0.22, 1, 0.36, 1), border-radius 260ms cubic-bezier(0.22, 1, 0.36, 1), box-shadow 260ms ease";
frame.style.transformOrigin =
config.position === "left" ? "left bottom" : "right bottom";
if (isMaximized) {
frame.style.top = "20px";
frame.style.right = "20px";
frame.style.bottom = "20px";
frame.style.left = "20px";
frame.style.width = "calc(100vw - 40px)";
frame.style.maxWidth = "none";
frame.style.height = "calc(100vh - 40px)";
frame.style.borderRadius = "24px";
return;
}
frame.style.top = "";
frame.style.bottom = "88px";
frame.style.right = config.position === "left" ? "" : "24px";
frame.style.left = config.position === "left" ? "24px" : "";
frame.style.width = config.width || "380px";
frame.style.maxWidth = "calc(100vw - 24px)";
frame.style.height = "min(760px, calc(100vh - 112px))";
frame.style.borderRadius = "28px";
}
function flushFrameState() {
if (!frame || !frameLoaded || !frameReady) {
return;
}
if (!initSent) {
initSent = true;
postToFrame({
type: "cs-agent:init",
payload: config,
});
}
postToFrame({ type: isOpen ? "cs-agent:open" : "cs-agent:minimize" });
postToFrame({
type: "cs-agent:maximized",
payload: { isMaximized: isMaximized },
});
}
function postToFrame(message) {
if (!frame || !frame.contentWindow) {
return;
}
try {
frame.contentWindow.postMessage(message, frameUrl.origin);
} catch (error) {
console.error("[cs-agent-widget] postMessage failed", error);
}
}
function syncFrameVisibility() {
if (!frame) {
return;
}
clearFrameTimers();
applyFrameLayout();
frame.style.display = "block";
if (isOpen) {
frame.style.visibility = "visible";
frame.style.pointerEvents = "auto";
frameHideTimer = window.setTimeout(function () {
if (!frame) {
return;
}
frame.style.opacity = "1";
frame.style.transform = "translate3d(0, 0, 0) scale(1)";
}, 16);
flushFrameState();
return;
}
frame.style.pointerEvents = "none";
frame.style.opacity = "0";
frame.style.transform = isMaximized
? "translate3d(0, 10px, 0) scale(0.985)"
: "translate3d(0, 16px, 0) scale(0.96)";
frameHideTimer = window.setTimeout(function () {
if (!frame || isOpen) {
return;
}
frame.style.visibility = "hidden";
}, animationDuration);
flushFrameState();
}
function handleWindowMessage(event) {
if (!frame || event.source !== frame.contentWindow) {
return;
}
var data = event.data || {};
if (data.type === "cs-agent:ready") {
frameReady = true;
flushFrameState();
return;
}
if (data.type === "cs-agent:request-minimize") {
isOpen = false;
syncFrameVisibility();
return;
}
if (data.type === "cs-agent:request-close") {
destroyFrame();
return;
}
if (data.type === "cs-agent:request-toggle-maximize") {
isMaximized = !isMaximized;
syncFrameVisibility();
}
}
function destroyFrame() {
if (!frame) {
return;
}
clearFrameTimers();
frame.style.pointerEvents = "none";
frame.style.opacity = "0";
frame.style.transform = "translate3d(0, 18px, 0) scale(0.94)";
frame.style.visibility = "hidden";
frameDestroyTimer = window.setTimeout(function () {
if (!frame) {
return;
}
if (frame.parentNode) {
frame.parentNode.removeChild(frame);
}
frame = null;
frameLoaded = false;
frameReady = false;
initSent = false;
isOpen = false;
isMaximized = false;
clearFrameTimers();
}, animationDuration);
}
function createFrame() {
if (frame) {
return frame;
}
frame = document.createElement("iframe");
frame.dataset.csAgentWidget = "frame";
frame.title = config.title || "在线客服";
frame.src = frameUrl.toString();
applyFrameLayout();
frame.style.display = "block";
frame.style.visibility = "hidden";
frame.style.pointerEvents = "none";
frame.style.opacity = "0";
frame.style.transform = "translate3d(0, 18px, 0) scale(0.96)";
frame.addEventListener("load", function () {
frameLoaded = true;
syncFrameVisibility();
});
document.body.appendChild(frame);
return frame;
}
button.addEventListener("click", function () {
if (!frame) {
createFrame();
}
isOpen = !isOpen;
syncFrameVisibility();
});
window.addEventListener("message", handleWindowMessage);
document.body.appendChild(button);
})();
+16
View File
@@ -0,0 +1,16 @@
import { generateUUID } from "@/lib/utils";
const EXTERNAL_ID_KEY = "cs-agent:external-id";
export function getOrCreateExternalId() {
if (typeof window === "undefined") {
return "";
}
const current = window.localStorage.getItem(EXTERNAL_ID_KEY);
if (current) {
return current;
}
const visitorId = `visitor_${generateUUID()}`;
window.localStorage.setItem(EXTERNAL_ID_KEY, visitorId);
return visitorId;
}