204 lines
6.4 KiB
JavaScript
204 lines
6.4 KiB
JavaScript
/**
|
||
* 客服云访客 Widget 嵌入脚本
|
||
* 用法: <script src="https://your-host/widget.js" data-id="WK_xxxx"></script>
|
||
*
|
||
* 向 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 = '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
|
||
|
||
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,
|
||
};
|
||
})();
|