Files
ai-kefu-jc/docs/superpowers/plans/2026-04-23-auto-popup-invite-card.md
T

628 lines
19 KiB
Markdown
Raw Normal View History

# 自动弹出邀请卡片实现计划
> **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` - 添加邀请卡片组件和逻辑
**新增代码段:**
1. 邀请卡片 HTML 模板
2. 邀请卡片 CSS 样式
3. 定时器管理
4. 存储管理
5. 事件处理器
6. 自动发送消息函数
7. 热门话题区域
---
## 任务 1: 添加邀请卡片存储管理
**文件:**
- 修改: `static/widget.js`
在现有变量声明区域(第 153-160 行附近)添加存储管理相关变量和函数:
- [ ] **Step 1: 添加存储管理变量**
在 `widget.js` 中找到 `var wSessionId = null;` 这一行(约第 155 行),在其后添加:
```javascript
// 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: 添加存储检查函数**
在同一区域添加存储管理函数:
```javascript
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**
```bash
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 定义之前添加:
```javascript
// 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 定义之前添加:
```javascript
var inviteEl = document.createElement("div");
inviteEl.className = "invite-container";
inviteEl.innerHTML = inviteTemplate;
inviteEl.style.display = "none"; // Hidden by default
```
- [ ] **Step 3: 将邀请卡片添加到容器**
找到 `container.appendChild(toggleBtn);` 这一行,在其之前添加:
```javascript
container.appendChild(inviteEl);
```
- [ ] **Step 4: 获取邀请卡片元素引用**
在现有元素引用定义区域(约第 161-165 行)添加:
```javascript
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**
```bash
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 行),在现有样式后、闭合标签前添加:
```javascript
// 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` 定位:
```css
".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**
```bash
git add static/widget.js
git commit -m "feat: add invite card CSS styles"
```
---
## 任务 4: 添加定时器逻辑
**文件:**
- 修改: `static/widget.js`
- [ ] **Step 1: 添加启动邀请定时器函数**
在存储管理函数后添加:
```javascript
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 行),在函数结束前添加:
```javascript
// Start invite timer
startInviteTimer();
```
- [ ] **Step 3: 添加定时器清理**
在 `toggle()` 函数中,当打开聊天面板时取消定时器:
找到 `toggle()` 函数定义(约第 169-173 行),修改为:
```javascript
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**
```bash
git add static/widget.js
git commit -m "feat: add invite timer logic (1.8s delay)"
```
---
## 任务 5: 添加邀请卡片事件处理器
**文件:**
- 修改: `static/widget.js`
- [ ] **Step 1: 添加关闭按钮事件**
在现有事件监听器区域(约第 175-177 行之后)添加:
```javascript
// Invite close button
inviteCloseBtn.addEventListener("click", function() {
hideInvite("temporary");
});
```
- [ ] **Step 2: 添加 Start Chat 按钮事件**
```javascript
// Invite Start Chat button
inviteStartBtn.addEventListener("click", function() {
hideInvite("temporary");
if (!isOpen) {
toggle();
}
});
```
- [ ] **Step 3: 添加 No Thanks 按钮事件**
```javascript
// Invite No Thanks button
inviteDismissBtn.addEventListener("click", function() {
hideInvite("permanent");
});
```
- [ ] **Step 4: 添加话题按钮事件**
```javascript
// 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**
```bash
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 行),修改为:
```javascript
'<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` 数组中添加:
```css
// 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()` 函数中添加热门话题按钮的事件监听:
```javascript
// 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()` 函数之前添加:
```javascript
function autoSendMessage(text) {
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
addMsg("user", text);
showTyping();
ws.send(JSON.stringify({ content: text }));
}
```
- [ ] **Step 5: 更新话题按钮使用新函数**
修改任务 5 中话题按钮的事件处理,使用新的辅助函数:
```javascript
// 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**
```bash
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`,打开它;否则创建一个简单的测试页面:
```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. ✅ 页面加载 1.8 秒后邀请卡片自动弹出
2. ✅ 邀请卡片样式正确(橙色头部、两个话题按钮)
3. ✅ 点击 "Get Quotation" → 邀请关闭 → 聊天打开 → 消息自动发送
4. ✅ 点击 "Distributor Cooperation" → 邀请关闭 → 聊天打开 → 消息自动发送
5. ✅ 点击 "Start Chat" → 邀请关闭 → 聊天打开
6. ✅ 点击 "No Thanks" → 邀请永久关闭
7. ✅ 点击悬浮按钮(邀请可见时)→ 邀请关闭 → 聊天打开
8. ✅ 点击悬浮按钮(邀请隐藏时)→ 聊天打开
- [ ] **Step 3: 测试页面独立性**
在多个标签页/页面中测试:
1. ✅ 页面 A 关闭邀请后,页面 B 仍能弹出邀请
2. ✅ 页面 A 刷新后,已关闭状态保持
3. ✅ 关闭整个网站重新打开,所有状态重置
- [ ] **Step 4: 测试热门话题**
1. ✅ 聊天面板显示 "POPULAR TOPICS" 区域
2. ✅ 点击话题按钮 → 消息自动发送
3. ✅ 发送第一条消息后,热门话题隐藏
- [ ] **Step 5: 测试响应式**
在不同屏幕尺寸下测试:
1. ✅ 移动端(<450px)样式正确
2. ✅ 话题按钮在小屏幕上正确排列
- [ ] **Step 6: 测试边界情况**
1. ✅ 快速连续点击悬浮按钮
2. ✅ 在邀请弹出前打开聊天
3. ✅ 在消息发送过程中关闭邀请
- [ ] **Step 7: Commit**
```bash
# 如果创建了测试文件
git add static/widget-test.html
git commit -m "test: add widget invite card test page"
```
---
## 任务 8: 最终清理和文档
- [ ] **Step 1: 代码审查**
检查代码:
1. ✅ 无 console.log 调试语句
2. ✅ 变量命名一致
3. ✅ 注释清晰
4. ✅ 无重复代码
- [ ] **Step 2: 浏览器兼容性测试**
在不同浏览器中测试:
1. ✅ Chrome/Edge (Chromium)
2. ✅ Firefox
3. ✅ Safari (如果可用)
- [ ] **Step 3: 性能检查**
1. ✅ 无内存泄漏(定时器清理)
2. ✅ 无不必要的 DOM 操作
3. ✅ 存储操作高效
- [ ] **Step 4: 最终 Commit**
```bash
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 行