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

492 lines
17 KiB
HTML
Raw 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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>演示商城 · 首页</title>
<style>
:root {
--brand: #2563eb;
--bg: #f8fafc;
--card: #fff;
--text: #0f172a;
--muted: #64748b;
--border: #e2e8f0;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
header.site {
background: #fff;
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 100;
}
.site-inner {
max-width: 960px;
margin: 0 auto;
padding: 0 20px;
height: 56px;
display: flex;
align-items: center;
gap: 24px;
}
.logo {
font-weight: 700;
font-size: 16px;
color: var(--brand);
text-decoration: none;
white-space: nowrap;
}
nav {
display: flex;
gap: 4px;
flex-wrap: wrap;
flex: 1;
}
nav a {
text-decoration: none;
color: var(--muted);
font-size: 14px;
padding: 6px 12px;
border-radius: 8px;
}
nav a:hover { background: #f1f5f9; color: var(--text); }
nav a.active {
background: #eff6ff;
color: var(--brand);
font-weight: 600;
}
main {
max-width: 960px;
margin: 0 auto;
padding: 24px 20px 120px;
}
.hero {
background: linear-gradient(135deg, #2563eb, #1d4ed8);
color: #fff;
border-radius: 16px;
padding: 32px 28px;
margin-bottom: 24px;
}
.hero h1 { margin: 0 0 8px; font-size: 24px; }
.hero p { margin: 0; opacity: 0.9; font-size: 14px; line-height: 1.6; }
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
margin-bottom: 16px;
}
.card h2 { margin: 0 0 8px; font-size: 18px; }
.card p { margin: 0 0 12px; color: var(--muted); font-size: 14px; line-height: 1.6; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
}
.product {
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
background: #fff;
cursor: pointer;
transition: border-color .15s, box-shadow .15s;
}
.product:hover {
border-color: #93c5fd;
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.08);
}
.product .thumb {
height: 100px;
border-radius: 8px;
background: linear-gradient(135deg, #dbeafe, #eff6ff);
margin-bottom: 10px;
}
.product .name { font-weight: 600; font-size: 14px; }
.product .price { color: var(--brand); font-weight: 700; margin-top: 4px; font-size: 15px; }
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
border: 0;
border-radius: 8px;
padding: 10px 16px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
background: var(--brand);
color: #fff;
}
.btn.secondary {
background: #fff;
color: var(--text);
border: 1px solid var(--border);
}
.btn + .btn { margin-left: 8px; }
.muted { color: var(--muted); font-size: 13px; }
.steps {
list-style: none;
padding: 0;
margin: 0;
counter-reset: step;
}
.steps li {
position: relative;
padding: 10px 12px 10px 40px;
border: 1px solid var(--border);
border-radius: 10px;
margin-bottom: 8px;
font-size: 13px;
line-height: 1.5;
background: #fff;
}
.steps li::before {
counter-increment: step;
content: counter(step);
position: absolute;
left: 12px;
top: 12px;
width: 20px;
height: 20px;
border-radius: 50%;
background: #eff6ff;
color: var(--brand);
font-size: 12px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.status-bar {
position: fixed;
left: 16px;
bottom: 16px;
z-index: 1000;
max-width: min(420px, calc(100vw - 100px));
background: rgba(15, 23, 42, 0.92);
color: #e2e8f0;
border-radius: 12px;
padding: 12px 14px;
font-size: 12px;
line-height: 1.5;
box-shadow: 0 8px 24px rgba(0,0,0,.2);
}
.status-bar strong { color: #fff; }
.status-bar code {
display: block;
margin-top: 4px;
padding: 6px 8px;
background: rgba(255,255,255,.08);
border-radius: 6px;
word-break: break-all;
color: #93c5fd;
font-size: 11px;
}
.status-bar .hint { color: #94a3b8; margin-top: 6px; font-size: 11px; }
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
background: #fef3c7;
color: #92400e;
font-size: 11px;
font-weight: 600;
}
label.field { display: block; font-size: 12px; color: var(--muted); margin-bottom: 4px; }
input.field {
width: 100%;
max-width: 320px;
height: 36px;
border: 1px solid var(--border);
border-radius: 8px;
padding: 0 10px;
font-size: 13px;
}
.toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: end; margin-top: 12px; }
</style>
</head>
<body>
<header class="site">
<div class="site-inner">
<a class="logo" href="#/" data-nav>演示商城</a>
<nav id="nav">
<a href="#/" data-nav data-path="/">首页</a>
<a href="#/product/iphone" data-nav data-path="/product/iphone">商品详情</a>
<a href="#/cart" data-nav data-path="/cart">购物车</a>
<a href="#/checkout" data-nav data-path="/checkout">结算</a>
<a href="#/help" data-nav data-path="/help">帮助中心</a>
</nav>
<span class="badge">访客轨迹测试站</span>
</div>
</header>
<main id="app"></main>
<div class="status-bar" id="statusBar">
<div><strong>当前宿主页(会上报给客服)</strong></div>
<code id="statusUrl"></code>
<div class="hint" id="statusTitle">title: —</div>
<div class="hint">
点右下角蓝钮打开客服 → 再点上方导航换页 → 在坐席工作台右侧看「落地页 / 浏览轨迹 / 在线时长」
</div>
</div>
<script>
(function () {
// 渠道 key?channel=WK_xxx ,默认种子渠道
var params = new URLSearchParams(location.search);
var channelKey = params.get('channel') || params.get('channel_key') || 'WK_8a3f2e';
var pages = {
'/': {
title: '演示商城 · 首页',
html: function () {
return (
'<div class="hero">' +
'<h1>欢迎来到演示商城</h1>' +
'<p>这是<strong>模拟客户官网</strong>。请打开右下角客服,再切换导航,验证:落地页、换页轨迹、在线读秒。</p>' +
'</div>' +
'<div class="card">' +
'<h2>测试步骤</h2>' +
'<ol class="steps">' +
'<li>确认后端 API 已启动(默认 :8080),前端 <code>npm run dev</code>:5173</li>' +
'<li>点击右下角蓝色客服按钮,发送一句「你好」</li>' +
'<li>保持聊天窗口打开,依次点:商品详情 → 购物车 → 结算 → 帮助中心</li>' +
'<li>坐席登录工作台,打开该会话,右侧查看落地页 / 浏览轨迹 / 在线时长</li>' +
'</ol>' +
'</div>' +
'<div class="card">' +
'<h2>热门商品</h2>' +
'<div class="grid">' +
productCard('iphone', '云客服专业版', '¥299/月') +
productCard('seat', '坐席扩容包', '¥99/席') +
productCard('api', '开放 API', '联系销售') +
'</div>' +
'</div>'
);
},
},
'/product/iphone': {
title: '演示商城 · 商品详情 · 云客服专业版',
html: function () {
return (
'<div class="card">' +
'<div class="thumb" style="height:160px;border-radius:12px;background:linear-gradient(135deg,#93c5fd,#2563eb);margin-bottom:16px"></div>' +
'<h2>云客服专业版</h2>' +
'<p class="muted">多渠道接待 · 知识库 · 数据统计 · 适合中小团队</p>' +
'<p style="font-size:22px;font-weight:700;color:#2563eb;margin:8px 0 16px">¥299 / 月</p>' +
'<button class="btn" type="button" data-goto="/cart">加入购物车</button>' +
'<button class="btn secondary" type="button" data-goto="/">返回首页</button>' +
'</div>' +
'<div class="card"><h2>规格说明</h2><p>本页用于模拟访客浏览商品详情。切换到此页应在客服端轨迹中新增一条记录。</p></div>'
);
},
},
'/product/seat': {
title: '演示商城 · 商品详情 · 坐席扩容包',
html: function () {
return (
'<div class="card">' +
'<h2>坐席扩容包</h2>' +
'<p>按需增加在线坐席数量。</p>' +
'<p style="font-size:22px;font-weight:700;color:#2563eb">¥99 / 席 / 月</p>' +
'<button class="btn" type="button" data-goto="/cart">加入购物车</button>' +
'</div>'
);
},
},
'/product/api': {
title: '演示商城 · 商品详情 · 开放 API',
html: function () {
return (
'<div class="card">' +
'<h2>开放 API</h2>' +
'<p>企业集成方案,请通过在线客服联系销售。</p>' +
'<button class="btn" type="button" onclick="window.KefuWidget&&KefuWidget.open()">咨询客服</button>' +
'</div>'
);
},
},
'/cart': {
title: '演示商城 · 购物车',
html: function () {
return (
'<div class="card">' +
'<h2>购物车</h2>' +
'<p>云客服专业版 × 1</p>' +
'<p class="muted">小计 ¥299</p>' +
'<button class="btn" type="button" data-goto="/checkout">去结算</button>' +
'<button class="btn secondary" type="button" data-goto="/product/iphone">继续浏览</button>' +
'</div>'
);
},
},
'/checkout': {
title: '演示商城 · 结算页',
html: function () {
return (
'<div class="card">' +
'<h2>确认订单</h2>' +
'<p>收货信息、支付方式等(示意)。访客停在结算页时,客服应能看到「当前页」为本页。</p>' +
'<button class="btn" type="button" onclick="alert(\'演示完成:请到坐席工作台查看浏览轨迹\')">提交订单(演示)</button>' +
'</div>'
);
},
},
'/help': {
title: '演示商城 · 帮助中心',
html: function () {
return (
'<div class="card">' +
'<h2>帮助中心</h2>' +
'<p>常见问题:如何退款?如何开通坐席?可点右下角咨询人工客服。</p>' +
'<button class="btn" type="button" onclick="window.KefuWidget&&KefuWidget.open()">联系客服</button>' +
'</div>' +
'<div class="card">' +
'<h2>渠道 Key</h2>' +
'<label class="field">当前 data-id / channel_key</label>' +
'<div class="toolbar">' +
'<input class="field" id="channelInput" value="' + escapeHtml(channelKey) + '" />' +
'<button class="btn secondary" type="button" id="applyChannel">切换并刷新</button>' +
'</div>' +
'<p class="muted" style="margin-top:10px">须与系统设置 → 渠道管理中的网页渠道 data-id 一致。种子默认为 <code>WK_8a3f2e</code>。</p>' +
'</div>'
);
},
},
};
function productCard(id, name, price) {
return (
'<div class="product" data-goto="/product/' + id + '">' +
'<div class="thumb"></div>' +
'<div class="name">' + name + '</div>' +
'<div class="price">' + price + '</div>' +
'</div>'
);
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/"/g, '&quot;');
}
function pathFromHash() {
var h = location.hash.replace(/^#/, '') || '/';
if (!h.startsWith('/')) h = '/' + h;
// strip query in hash
return h.split('?')[0];
}
function updateStatus() {
var elUrl = document.getElementById('statusUrl');
var elTitle = document.getElementById('statusTitle');
if (elUrl) elUrl.textContent = location.href;
if (elTitle) elTitle.textContent = 'title: ' + document.title;
}
function setActiveNav(path) {
document.querySelectorAll('#nav a').forEach(function (a) {
var p = a.getAttribute('data-path');
a.classList.toggle('active', p === path || (path.indexOf('/product/') === 0 && p === '/product/iphone' && path === '/product/iphone'));
if (path.indexOf('/product/') === 0) {
a.classList.toggle('active', p === path || (p === '/product/iphone' && path === '/product/iphone'));
}
});
// simpler: exact match
document.querySelectorAll('#nav a').forEach(function (a) {
a.classList.toggle('active', a.getAttribute('data-path') === path);
});
}
function render() {
var path = pathFromHash();
var page = pages[path] || pages['/'];
if (!pages[path]) {
// unknown product path fallback
if (path.indexOf('/product/') === 0) {
page = {
title: '演示商城 · 商品 ' + path,
html: function () {
return '<div class="card"><h2>商品 ' + escapeHtml(path) + '</h2><p>示意详情页</p><button class="btn" data-goto="/cart">加入购物车</button></div>';
},
};
} else {
path = '/';
page = pages['/'];
}
}
document.title = page.title;
document.getElementById('app').innerHTML = page.html();
setActiveNav(path);
updateStatus();
// 绑定站内跳转(触发 history / hashwidget.js 会监听到)
document.querySelectorAll('[data-goto]').forEach(function (el) {
el.addEventListener('click', function (e) {
e.preventDefault();
go(el.getAttribute('data-goto'));
});
});
var applyBtn = document.getElementById('applyChannel');
if (applyBtn) {
applyBtn.addEventListener('click', function () {
var v = (document.getElementById('channelInput').value || '').trim();
if (!v) return;
var u = new URL(location.href);
u.searchParams.set('channel', v);
location.href = u.pathname + u.search + '#/help';
});
}
}
function go(path) {
if (!path) return;
if (path.charAt(0) !== '/') path = '/' + path;
// 使用 pushState + hashURL 变化更接近真实站点路径展示
// 最终 href 形如 /demo-shop/?channel=xx#/cart
var next = '#' + path;
if (location.hash !== next) {
location.hash = path;
} else {
render();
}
}
window.addEventListener('hashchange', render);
// 兼容直接点 nav 的 a[href="#/..."]
document.querySelectorAll('a[data-nav]').forEach(function (a) {
a.addEventListener('click', function () {
setTimeout(updateStatus, 0);
});
});
render();
// 动态插入 widget.js(与渠道 key 一致)
var s = document.createElement('script');
s.src = '/widget.js';
s.setAttribute('data-id', channelKey);
s.async = true;
s.onerror = function () {
alert('加载 /widget.js 失败。请用前端开发服务打开本页,例如:\nhttp://localhost:5173/demo-shop/');
};
document.body.appendChild(s);
// 标题/状态栏同步
setInterval(updateStatus, 1000);
})();
</script>
</body>
</html>