492 lines
17 KiB
HTML
492 lines
17 KiB
HTML
<!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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
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 / hash,widget.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 + hash,URL 变化更接近真实站点路径展示
|
||
// 最终 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>
|