Co-Authored-By: Claude Code <noreply@anthropic.com>
19 KiB
自动弹出邀请卡片实现计划
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
目标: 为 Widget 模式添加自动弹出邀请卡片,页面加载 1.8 秒后显示,提供快速访问热门话题的入口
架构: 在现有 widget.js 中扩展,使用 Shadow DOM 隔离样式,sessionStorage 管理每页独立的关闭状态
技术栈: 原生 JavaScript、Shadow DOM、sessionStorage
文件结构
修改文件:
static/widget.js- 添加邀请卡片组件和逻辑
新增代码段:
- 邀请卡片 HTML 模板
- 邀请卡片 CSS 样式
- 定时器管理
- 存储管理
- 事件处理器
- 自动发送消息函数
- 热门话题区域
任务 1: 添加邀请卡片存储管理
文件:
- 修改:
static/widget.js
在现有变量声明区域(第 153-160 行附近)添加存储管理相关变量和函数:
- Step 1: 添加存储管理变量
在 widget.js 中找到 var wSessionId = null; 这一行(约第 155 行),在其后添加:
// Invite card state management
var inviteTimer = null;
var invitePanel = null;
var inviteShown = false;
var inviteStorageKey = "widget_invite_dismissed_" + window.location.pathname;
var isPermanentlyClosed = false;
- Step 2: 添加存储检查函数
在同一区域添加存储管理函数:
function checkInviteStorage() {
var dismissed = sessionStorage.getItem(inviteStorageKey);
if (dismissed === "permanent") {
isPermanentlyClosed = true;
return true;
}
return false;
}
function setInviteDismissed(type) {
if (type === "permanent") {
sessionStorage.setItem(inviteStorageKey, "permanent");
isPermanentlyClosed = true;
} else {
sessionStorage.setItem(inviteStorageKey, "temporary");
}
}
function clearInviteDismissed() {
sessionStorage.removeItem(inviteStorageKey);
isPermanentlyClosed = false;
}
- Step 3: Commit
git add static/widget.js
git commit -m "feat: add invite card storage management functions"
任务 2: 添加邀请卡片 HTML 模板
文件:
-
修改:
static/widget.js -
Step 1: 添加邀请卡片 HTML 模板
在 widget.js 中找到聊天面板的 HTML 定义(约第 115-134 行,panel.innerHTML = [...]),在其后、toggleBtn 定义之前添加:
// Invite card HTML template
var inviteTemplate = [
'<div class="invite-panel">',
'<div class="invite-header">',
'<div class="invite-icon-wrapper">',
'<div class="invite-icon">🤖</div>',
'<span class="invite-status-dot"></span>',
'</div>',
'<div class="invite-header-content">',
'<div class="invite-title">Get stock list or quotation now</div>',
'<div class="invite-subtitle">Chat with us for available vehicles, delivery timing and distributor cooperation.</div>',
'</div>',
'<button class="invite-close-btn" aria-label="Close">',
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>',
'</button>',
'</div>',
'<div class="invite-body">',
'<div class="invite-topics">',
'<button class="invite-topic-btn" data-topic="Get Quotation">Get Quotation</button>',
'<button class="invite-topic-btn" data-topic="Distributor Cooperation">Distributor Cooperation</button>',
'</div>',
'<div class="invite-actions">',
'<button class="invite-start-btn">Start Chat',
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"></polyline></svg>',
'</button>',
'<button class="invite-dismiss-btn">No Thanks</button>',
'</div>',
'</div>',
'</div>'
].join("");
- Step 2: 创建邀请卡片 DOM 元素
在 toggleBtn 定义之前添加:
var inviteEl = document.createElement("div");
inviteEl.className = "invite-container";
inviteEl.innerHTML = inviteTemplate;
inviteEl.style.display = "none"; // Hidden by default
- Step 3: 将邀请卡片添加到容器
找到 container.appendChild(toggleBtn); 这一行,在其之前添加:
container.appendChild(inviteEl);
- Step 4: 获取邀请卡片元素引用
在现有元素引用定义区域(约第 161-165 行)添加:
var inviteCloseBtn = shadow.querySelector(".invite-close-btn");
var inviteStartBtn = shadow.querySelector(".invite-start-btn");
var inviteDismissBtn = shadow.querySelector(".invite-dismiss-btn");
var inviteTopicBtns = shadow.querySelectorAll(".invite-topic-btn");
- Step 5: Commit
git add static/widget.js
git commit -m "feat: add invite card HTML template and DOM structure"
任务 3: 添加邀请卡片 CSS 样式
文件:
-
修改:
static/widget.js -
Step 1: 添加邀请卡片基础样式
在 style.textContent 数组中(约第 38-106 行),在现有样式后、闭合标签前添加:
// Invite card container
".invite-container { position:fixed; z-index:99998; }",
".invite-panel { width:360px; max-width:calc(100vw - 24px); overflow:hidden; border-radius:26px; border:1px solid #F1CCAE; background:white; box-shadow:0 28px 80px rgba(196,91,25,0.22); opacity:0; transform:translateY(20px); transition:opacity 0.3s, transform 0.3s; pointer-events:none; }",
".invite-panel.show { opacity:1; transform:translateY(0); pointer-events:auto; }",
".invite-panel.hide { display:none; }",
// Invite header
".invite-header { background:linear-gradient(135deg, #F08A34 0%, #EC8C2F 100%); padding:20px; color:white; display:flex; align-items:flex-start; gap:12px; }",
".invite-icon-wrapper { position:relative; flex-shrink:0; }",
".invite-icon { width:44px; height:44px; background:rgba(255,255,255,0.18); border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:20px; backdrop-filter:blur(10px); }",
".invite-status-dot { position:absolute; bottom:-2px; right:-2px; width:14px; height:14px; background:#22c55e; border-radius:50%; border:3px solid #F08A34; }",
".invite-header-content { flex:1; min-width:0; }",
".invite-title { font-size:16px; font-weight:600; line-height:1.4; }",
".invite-subtitle { font-size:13px; margin-top:4px; opacity:0.9; line-height:1.5; }",
".invite-close-btn { flex-shrink:0; width:32px; height:32px; border-radius:8px; background:rgba(255,255,255,0.1); border:none; color:white; cursor:pointer; display:flex; align-items:center; justify-content:center; transition:background 0.2s; }",
".invite-close-btn:hover { background:rgba(255,255,255,0.2); }",
// Invite body
".invite-body { padding:16px 20px; background:#FFF9F4; }",
// Topic buttons
".invite-topics { display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-bottom:16px; }",
".invite-topic-btn { border-radius:22px; border:1px solid #EECFB6; background:white; padding:14px 16px; text-align:center; font-size:13px; font-weight:500; color:#8C4D1D; cursor:pointer; transition:background 0.2s; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }",
".invite-topic-btn:hover { background:#FFF7F1; }",
// Action buttons
".invite-actions { display:flex; gap:10px; }",
".invite-start-btn { flex:1; border-radius:20px; border:none; background:linear-gradient(135deg, #F08A34 0%, #EC8C2F 100%); padding:14px 20px; color:white; font-size:14px; font-weight:600; cursor:pointer; display:flex; align-items:center; justify-content:center; gap:8px; box-shadow:0 2px 8px rgba(240,138,49,0.25); transition:opacity 0.2s; }",
".invite-start-btn:hover { opacity:0.95; }",
".invite-dismiss-btn { border-radius:20px; border:1px solid #E8C4A9; background:white; padding:14px 20px; color:#A8591F; font-size:14px; font-weight:500; cursor:pointer; transition:background 0.2s; }",
".invite-dismiss-btn:hover { background:#FEFEFE; }",
// Responsive
"@media(max-width:450px){ .invite-panel{width:calc(100vw - 24px);} .invite-topics{grid-template-columns:1fr;} }",
确保在数组最后添加逗号(除了最后一项)。
- Step 2: 调整现有聊天面板和悬浮按钮样式
找到 .chat-panel 样式定义,修改其 bottom 定位:
".chat-panel { position:absolute; bottom:80px; right:0; width:400px; height:520px; background:" + c.bg + "; border-radius:16px; box-shadow:0 10px 40px rgba(0,0,0,0.15); display:none; flex-direction:column; overflow:hidden; }",
修改 bottom:70px 为 bottom:80px,为邀请卡片留出空间。
- Step 3: Commit
git add static/widget.js
git commit -m "feat: add invite card CSS styles"
任务 4: 添加定时器逻辑
文件:
-
修改:
static/widget.js -
Step 1: 添加启动邀请定时器函数
在存储管理函数后添加:
function startInviteTimer() {
if (checkInviteStorage()) {
return; // Already dismissed, don't show
}
inviteTimer = window.setTimeout(function() {
if (!isOpen) {
showInvite();
}
}, 1800); // 1.8 seconds
}
function showInvite() {
if (inviteShown || isPermanentlyClosed) return;
inviteShown = true;
invitePanel = shadow.querySelector(".invite-panel");
if (invitePanel) {
invitePanel.classList.remove("hide");
invitePanel.classList.add("show");
}
}
function hideInvite(type) {
if (!invitePanel) return;
invitePanel.classList.remove("show");
invitePanel.classList.add("hide");
setInviteDismissed(type || "temporary");
}
- Step 2: 在 init 函数末尾启动定时器
找到 init() 函数的末尾(约第 265 行),在函数结束前添加:
// Start invite timer
startInviteTimer();
- Step 3: 添加定时器清理
在 toggle() 函数中,当打开聊天面板时取消定时器:
找到 toggle() 函数定义(约第 169-173 行),修改为:
function toggle() {
isOpen = !isOpen;
panel.classList.toggle("open", isOpen);
if (isOpen) {
if (inviteTimer) {
window.clearTimeout(inviteTimer);
inviteTimer = null;
}
hideInvite("temporary");
if (!wWs) wConnect();
} else {
// Only restart timer if not permanently closed
if (!isPermanentlyClosed && !checkInviteStorage()) {
inviteShown = false;
startInviteTimer();
}
}
}
- Step 4: Commit
git add static/widget.js
git commit -m "feat: add invite timer logic (1.8s delay)"
任务 5: 添加邀请卡片事件处理器
文件:
-
修改:
static/widget.js -
Step 1: 添加关闭按钮事件
在现有事件监听器区域(约第 175-177 行之后)添加:
// Invite close button
inviteCloseBtn.addEventListener("click", function() {
hideInvite("temporary");
});
- Step 2: 添加 Start Chat 按钮事件
// Invite Start Chat button
inviteStartBtn.addEventListener("click", function() {
hideInvite("temporary");
if (!isOpen) {
toggle();
}
});
- Step 3: 添加 No Thanks 按钮事件
// Invite No Thanks button
inviteDismissBtn.addEventListener("click", function() {
hideInvite("permanent");
});
- Step 4: 添加话题按钮事件
// Invite topic buttons - auto send message
Array.prototype.forEach.call(inviteTopicBtns, function(btn) {
btn.addEventListener("click", function() {
var topic = this.getAttribute("data-topic");
hideInvite("temporary");
if (!isOpen) {
toggle();
}
// Auto send message after chat opens
setTimeout(function() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ content: topic }));
addMsg("user", topic);
inputEl.value = "";
showTyping();
}
}, 300);
});
});
- Step 5: Commit
git add static/widget.js
git commit -m "feat: add invite card event handlers"
任务 6: 添加聊天面板热门话题区域
文件:
-
修改:
static/widget.js -
Step 1: 修改消息区域 HTML
找到 panel.innerHTML 定义中的消息区域部分(约第 124-127 行),修改为:
'<div class="panel-messages" id="w-msgs">',
'<div class="help-block"><div style="display:flex;align-items:center;gap:14px;"><div class="help-content"><div class="help-title">How can we help?</div><div class="help-description">Ask about vehicles, market support, quotation, shipping and delivery.</div></div><a href="https://wa.me/+8619037179250" target="_blank" class="call-btn"><span style="font-size:15px;">📞</span><span>Call us</span></a></div></div>',
'<div class="popular-topics" id="w-popular-topics"><div class="popular-topics-label">POPULAR TOPICS</div><div class="popular-topics-grid"><button class="popular-topic-btn" data-topic="Get Quotation">Get Quotation</button><button class="popular-topic-btn" data-topic="Distributor Cooperation">Distributor Cooperation</button></div></div>',
'<div class="msg-wrapper"><div class="msg ai">Hey! What kind of car are you looking for?</div></div>',
'</div>',
- Step 2: 添加热门话题 CSS 样式
在 style.textContent 数组中添加:
// Popular Topics
".popular-topics { margin-bottom:16px; }",
".popular-topics-label { font-size:11px; font-weight:600; color:#C16C2E; letter-spacing:0.14em; margin-bottom:8px; text-transform:uppercase; }",
".popular-topics-grid { display:flex; gap:8px; flex-wrap:wrap; }",
".popular-topic-btn { border-radius:20px; border:1px solid #F0D5BF; background:#FFF8F3; padding:8px 16px; font-size:12px; font-weight:500; color:#8C4D1D; cursor:pointer; transition:background 0.2s; white-space:nowrap; }",
".popular-topic-btn:hover { background:white; }",
- Step 3: 添加热门话题事件处理
在 init() 函数中添加热门话题按钮的事件监听:
// Popular topics buttons
var popularTopicBtns = shadow.querySelectorAll(".popular-topic-btn");
Array.prototype.forEach.call(popularTopicBtns, function(btn) {
btn.addEventListener("click", function() {
var topic = this.getAttribute("data-topic");
// Hide popular topics after first interaction
var popularTopicsEl = shadow.getElementById("w-popular-topics");
if (popularTopicsEl) {
popularTopicsEl.style.display = "none";
}
// Send message
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ content: topic }));
addMsg("user", topic);
inputEl.value = "";
showTyping();
}
});
});
- Step 4: 添加自动发送消息辅助函数
在 wSend() 函数之前添加:
function autoSendMessage(text) {
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
addMsg("user", text);
showTyping();
ws.send(JSON.stringify({ content: text }));
}
- Step 5: 更新话题按钮使用新函数
修改任务 5 中话题按钮的事件处理,使用新的辅助函数:
// Invite topic buttons - auto send message
Array.prototype.forEach.call(inviteTopicBtns, function(btn) {
btn.addEventListener("click", function() {
var topic = this.getAttribute("data-topic");
hideInvite("temporary");
if (!isOpen) {
toggle();
}
// Auto send message after chat opens
setTimeout(function() {
autoSendMessage(topic);
}, 300);
});
});
- Step 6: Commit
git add static/widget.js
git commit -m "feat: add popular topics section to chat panel"
任务 7: 测试和验证
文件:
-
测试:
static/widget-test.html或在浏览器中测试 -
Step 1: 创建测试页面
如果有 widget-test.html,打开它;否则创建一个简单的测试页面:
<!DOCTYPE html>
<html>
<head>
<title>Widget Invite Test</title>
</head>
<body>
<h1>Widget Invite Card Test</h1>
<p>Invite should appear after 1.8 seconds</p>
<script src="/static/widget.js" data-position="bottom-right" data-theme="light"></script>
</body>
</html>
- Step 2: 测试基本功能
打开测试页面,验证:
- ✅ 页面加载 1.8 秒后邀请卡片自动弹出
- ✅ 邀请卡片样式正确(橙色头部、两个话题按钮)
- ✅ 点击 "Get Quotation" → 邀请关闭 → 聊天打开 → 消息自动发送
- ✅ 点击 "Distributor Cooperation" → 邀请关闭 → 聊天打开 → 消息自动发送
- ✅ 点击 "Start Chat" → 邀请关闭 → 聊天打开
- ✅ 点击 "No Thanks" → 邀请永久关闭
- ✅ 点击悬浮按钮(邀请可见时)→ 邀请关闭 → 聊天打开
- ✅ 点击悬浮按钮(邀请隐藏时)→ 聊天打开
- Step 3: 测试页面独立性
在多个标签页/页面中测试:
- ✅ 页面 A 关闭邀请后,页面 B 仍能弹出邀请
- ✅ 页面 A 刷新后,已关闭状态保持
- ✅ 关闭整个网站重新打开,所有状态重置
- Step 4: 测试热门话题
- ✅ 聊天面板显示 "POPULAR TOPICS" 区域
- ✅ 点击话题按钮 → 消息自动发送
- ✅ 发送第一条消息后,热门话题隐藏
- Step 5: 测试响应式
在不同屏幕尺寸下测试:
- ✅ 移动端(<450px)样式正确
- ✅ 话题按钮在小屏幕上正确排列
- Step 6: 测试边界情况
- ✅ 快速连续点击悬浮按钮
- ✅ 在邀请弹出前打开聊天
- ✅ 在消息发送过程中关闭邀请
- Step 7: Commit
# 如果创建了测试文件
git add static/widget-test.html
git commit -m "test: add widget invite card test page"
任务 8: 最终清理和文档
- Step 1: 代码审查
检查代码:
- ✅ 无 console.log 调试语句
- ✅ 变量命名一致
- ✅ 注释清晰
- ✅ 无重复代码
- Step 2: 浏览器兼容性测试
在不同浏览器中测试:
- ✅ Chrome/Edge (Chromium)
- ✅ Firefox
- ✅ Safari (如果可用)
- Step 3: 性能检查
- ✅ 无内存泄漏(定时器清理)
- ✅ 无不必要的 DOM 操作
- ✅ 存储操作高效
- Step 4: 最终 Commit
git add static/widget.js
git commit -m "chore: final cleanup and polish invite card feature"
验收标准
功能要求
- ✅ 页面加载 1.8 秒后自动弹出邀请卡片
- ✅ 包含两个话题按钮:Get Quotation、Distributor Cooperation
- ✅ 点击话题按钮 → 关闭邀请 → 打开聊天 → 自动发送消息
- ✅ 点击 "Start Chat" → 关闭邀请 → 打开聊天
- ✅ 点击 "No Thanks" → 永久关闭邀请
- ✅ 每个页面独立管理状态
- ✅ 聊天面板显示热门话题区域
样式要求
- ✅ 橙色渐变头部:
linear-gradient(135deg, #F08A34 0%, #EC8C2F 100%) - ✅ 圆角 26px
- ✅ 阴影效果正确
- ✅ 淡入 + 滑入动画
- ✅ 响应式设计
技术要求
- ✅ 使用 sessionStorage 存储状态
- ✅ 使用 Shadow DOM 隔离样式
- ✅ 定时器正确清理
- ✅ 无内存泄漏
预计工作量: 约 2-3 小时 文件修改: 1 个文件(static/widget.js) 新增代码: 约 250-300 行