Files
yml2213 970bae16d1 修复访客浏览轨迹延迟与排序,增加演示站
保留 SPA hash 以正确区分页面;延迟采集 title 避免慢一页;轨迹改为最新在上;补充 demo-shop 测试页与 Vite 静态路由。
2026-07-18 23:22:47 +08:00

204 lines
6.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 客服云访客 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,
};
})();