Files
ai-kefu-jc/docs/superpowers/plans/2026-04-23-auto-popup-invite-card.md
T
2026-09-29 14:15:12 +08:00

628 lines
19 KiB
Markdown
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.
# 自动弹出邀请卡片实现计划
> **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 行