/** * 客服云访客 Widget 嵌入脚本 * 用法: * * 向 iframe 同步宿主页 URL / 标题,并监听 SPA 路由变化,供客服端展示落地页与浏览轨迹。 */ (function () { if (window.__KEFU_WIDGET_LOADED__) return; window.__KEFU_WIDGET_LOADED__ = true; var script = document.currentScript; if (!script) { var scripts = document.getElementsByTagName('script'); script = scripts[scripts.length - 1]; } var channelKey = (script && script.getAttribute('data-id')) || ''; if (!channelKey) { console.warn('[kefu-widget] missing data-id on script tag'); return; } var src = script && script.src ? script.src : ''; var base = ''; try { var u = new URL(src, window.location.href); base = u.origin; } catch (e) { base = window.location.origin; } var open = false; var iframe = null; var lastSentKey = ''; var lastSentAt = 0; var routeTimer = null; var btn = document.createElement('button'); btn.type = 'button'; btn.setAttribute('aria-label', '打开在线客服'); btn.style.cssText = [ 'position:fixed', 'right:24px', 'bottom:24px', 'z-index:2147483000', 'width:56px', 'height:56px', 'border:none', 'border-radius:9999px', 'background:#2563eb', 'color:#fff', 'cursor:pointer', 'box-shadow:0 8px 24px rgba(0,0,0,0.12)', 'display:flex', 'align-items:center', 'justify-content:center', 'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif', ].join(';'); btn.innerHTML = ''; var panel = document.createElement('div'); panel.style.cssText = [ 'position:fixed', 'right:24px', 'bottom:24px', 'z-index:2147483001', 'width:400px', 'height:600px', 'max-width:calc(100vw - 32px)', 'max-height:calc(100vh - 32px)', 'border-radius:12px', 'overflow:hidden', 'box-shadow:0 8px 24px rgba(0,0,0,0.12)', 'display:none', 'background:#fff', ].join(';'); function hostPageInfo() { var href = ''; var title = ''; var ref = ''; try { href = String(window.location.href || ''); title = String(document.title || ''); ref = String(document.referrer || ''); } catch (e) { /* ignore */ } return { url: href, title: title, referrer: ref }; } function buildEmbedURL() { var info = hostPageInfo(); var q = 'channel_key=' + encodeURIComponent(channelKey) + '&embedded=1' + '&page_url=' + encodeURIComponent(info.url) + '&page_title=' + encodeURIComponent(info.title) + '&referrer=' + encodeURIComponent(info.referrer); return base + '/widget/embed?' + q; } function postPageToIframe(force) { if (!iframe || !iframe.contentWindow) return; var info = hostPageInfo(); var now = Date.now(); // url + title 一起去重:SPA 常先改 hash 再改 title,只比 url 会丢掉正确标题 var key = info.url + '\0' + info.title; if (!force && key === lastSentKey && now - lastSentAt < 500) return; lastSentKey = key; lastSentAt = now; try { iframe.contentWindow.postMessage({ type: 'kefu-host-page', url: info.url, title: info.title, referrer: info.referrer, }, '*'); } catch (e) { /* ignore */ } } /** * 路由变化后延迟采集:让宿主 SPA(含 demo-shop)先跑完 render、更新 document.title, * 避免轨迹标题永远慢一页(URL 已是新页、title 仍是上一页)。 */ function schedulePostPage(force) { if (!open && !force) return; if (routeTimer) clearTimeout(routeTimer); routeTimer = setTimeout(function () { routeTimer = null; postPageToIframe(!!force); // 再补一帧:部分框架 title 在 paint 后才写 setTimeout(function () { postPageToIframe(false); }, 50); }, 0); } function ensureIframe() { if (iframe) return; iframe = document.createElement('iframe'); iframe.title = '在线客服'; iframe.allow = 'clipboard-write'; iframe.style.cssText = 'width:100%;height:100%;border:0;display:block;background:#fff;'; iframe.src = buildEmbedURL(); iframe.addEventListener('load', function () { schedulePostPage(true); }); panel.appendChild(iframe); } function setOpen(next) { open = next; if (open) { ensureIframe(); panel.style.display = 'block'; btn.style.display = 'none'; schedulePostPage(true); } else { panel.style.display = 'none'; btn.style.display = 'flex'; } } btn.addEventListener('click', function () { setOpen(true); }); window.addEventListener('message', function (event) { if (!event || !event.data) return; if (event.data.type === 'kefu-widget-close' || event.data.type === 'kefu-widget-minimize') { setOpen(false); } // iframe 就绪后可再次同步宿主页 if (event.data.type === 'kefu-widget-ready') { schedulePostPage(true); } }); // —— SPA / 浏览器导航监听 —— function onRouteMaybeChanged() { if (!open) return; schedulePostPage(false); } try { var _push = history.pushState; var _replace = history.replaceState; history.pushState = function () { var r = _push.apply(this, arguments); onRouteMaybeChanged(); return r; }; history.replaceState = function () { var r = _replace.apply(this, arguments); onRouteMaybeChanged(); return r; }; } catch (e) { /* ignore */ } window.addEventListener('popstate', onRouteMaybeChanged); window.addEventListener('hashchange', onRouteMaybeChanged); // 标题可能异步更新 try { var titleEl = document.querySelector('title'); if (titleEl && typeof MutationObserver !== 'undefined') { new MutationObserver(function () { onRouteMaybeChanged(); }).observe(titleEl, { subtree: true, characterData: true, childList: true, }); } } catch (e) { /* ignore */ } function mount() { document.body.appendChild(btn); document.body.appendChild(panel); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', mount); } else { mount(); } window.KefuWidget = { open: function () { setOpen(true); }, close: function () { setOpen(false); }, channelKey: channelKey, }; })();