Co-Authored-By: Claude Code <noreply@anthropic.com>
18 KiB
邀请卡自动弹出开关 + 移动端悬浮按钮 Implementation Plan
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.
Goal: 为嵌入式组件 widget.js 新增两个 data-* 配置开关——data-auto-invite(控制邀请卡是否 1.8s 自动弹出)与 data-mobile(切换到移动端竖排悬浮按钮 D2),默认行为 100% 向后兼容。
Architecture: 复用 widget.js 既有的 script.getAttribute("data-*") 配置范式。data-auto-invite 在 startInviteTimer() 开头加一句短路;data-mobile 走「独立移动端 DOM 模板」分支(桌面模板逐字不动),并配套一份贴角的位置表 positionStylesMobile 与移动端 CSS。
Tech Stack: 原生 JavaScript(ES5 风格 IIFE + Shadow DOM)、静态 HTML。无构建步骤、无打包器。
Spec: docs/superpowers/specs/2026-06-23-auto-invite-toggle-and-mobile-button-design.md
⚠️ 关于测试方式(重要)
本仓库为纯静态 JS 前端,没有任何 JS 测试框架(无 package.json,tests/ 只有空的 __init__.py)。为本改动引入 JS 测试框架违反 YAGNI。
因此本计划不使用自动化 TDD,而是采用手动浏览器验证:每个 Task 末尾用浏览器打开页面、通过 DevTools 检查实际 DOM / 行为。验证标准写明「期望看到的结果」,与自动化断言等价。
验证通用方法:
- 方式 A(推荐,贴近真实部署):启动后端服务(
bash deploy.sh或docker compose up),浏览器访问页面。 - 方式 B(最快,免启动服务):直接用浏览器打开本地 HTML 文件(
file://)。组件的 WebSocket 会连不上,但不影响本特性的验证——邀请卡弹出与按钮渲染都在初始化时完成,与 WS 无关。 - 检查 Shadow DOM 内部:DevTools → Elements → 展开组件 Shadow Root,或在 Console 执行下文给出的命令。
文件结构(File Structure)
| 文件 | 职责 | 本次改动 |
|---|---|---|
static/widget.js |
嵌入式组件(第三方站点通过 <script> 引入,渲染 Shadow DOM 聊天窗) |
读取 2 个新属性 + 邀请卡短路 1 行 + 悬浮按钮分两套模板 + 新增移动端 CSS + 新增 positionStylesMobile |
static/widget-test.html |
嵌入式组件的本地测试/演示页 | 给组件 <script> 标签加 data-mobile="true"(演示 D2 按钮) |
docs/test-checklist.md |
手动测试清单 | 补充本次两个开关的验证条目 |
边界说明:本次只改 widget.js(嵌入式组件)。独立聊天页 chat.html / chat.js 不在范围内——独立页没有邀请卡逻辑,悬浮按钮也是 widget 独有概念(见 spec §1.3)。
Task 1: data-auto-invite —— 邀请卡自动弹出开关
Files:
-
Modify:
static/widget.js(IIFE 顶部新增autoInvite读取) -
Modify:
static/widget.js(startInviteTimer()开头新增短路) -
Step 1: 新增
autoInvite配置读取
在 static/widget.js 第 13 行 var email = ... 之后、第 14 行 var wsBase = new URL(script.src).origin; 之前,插入一行:
var autoInvite = script.getAttribute("data-auto-invite") !== "false";
插入后该区域应为:
var whatsapp = script.getAttribute("data-whatsapp") || "https://wa.me/+8619037179250";
var email = script.getAttribute("data-email") || "mailto:info@borderlesscar.com";
var autoInvite = script.getAttribute("data-auto-invite") !== "false";
var wsBase = new URL(script.src).origin;
语义:属性缺失、空串或任何非
"false"值 →autoInvite = true(保持现状自动弹);仅data-auto-invite="false"→false。
- Step 2:
startInviteTimer()开头加短路
定位 startInviteTimer 函数(约第 306 行),当前为:
function startInviteTimer() {
if (checkInviteStorage()) {
return; // Already dismissed, don't show
}
改为(在函数体最开头插入一行 if (!autoInvite) return;):
function startInviteTimer() {
if (!autoInvite) return; // 配置关闭则不启动计时器,邀请卡永不自动弹
if (checkInviteStorage()) {
return; // Already dismissed, don't show
}
startInviteTimer定义在init()内部(嵌套函数),autoInvite在 IIFE 顶层(Step 1),闭包可访问,无需传参。
- Step 3: 手动验证——默认值(向后兼容,邀请卡仍弹)
widget-test.html 此刻未加 data-auto-invite,等于「未提供」→ autoInvite=true。
打开 static/widget-test.html(方式 A 或 B),等待约 2 秒。
Expected:邀请卡在 ~1.8s 后自动弹出(橙色渐变 header、🤖 图标、两个 topic 按钮),与改动前一致。
- Step 4: 手动验证——
data-auto-invite="false"不弹(临时)
临时编辑 static/widget-test.html:69 的组件 script 标签,加上 data-auto-invite="false":
<script src="/static/widget.js" data-position="bottom-right" data-theme="light" data-whatsapp="https://wa.me/8613800138000" data-email="mailto:demo@hesu-auto.com" data-auto-invite="false"></script>
刷新页面,等待 5 秒以上。 Expected:邀请卡不再自动弹出;悬浮按钮照常显示,点击仍能打开聊天面板。
验证通过后,把 data-auto-invite="false" 从该标签删掉(恢复成 Step 3 的状态,保持测试页的邀请卡回归用途)。Task 3 不会重新加它。
- Step 5: 提交
git add static/widget.js
git commit -m "$(cat <<'EOF'
feat(widget): 新增 data-auto-invite 控制邀请卡自动弹出
默认(true)保持 1.8s 自动弹出;设 data-auto-invite="false" 时
startInviteTimer 直接返回,邀请卡不再自动弹,悬浮按钮照常可用。
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
Task 2: data-mobile —— 移动端竖排悬浮按钮(D2)+ 贴角位置
Files:
-
Modify:
static/widget.js(IIFE 顶部新增isMobile读取 +positionStylesMobile+pos选择) -
Modify:
static/widget.js(样式数组新增移动端 CSS) -
Modify:
static/widget.js(toggleBtn改为按isMobile分两套模板) -
Step 1: 新增
isMobile配置读取
在 Task 1 新增的 var autoInvite = ... 之后、var wsBase = ... 之前,插入:
var isMobile = script.getAttribute("data-mobile") === "true";
插入后该区域应为:
var autoInvite = script.getAttribute("data-auto-invite") !== "false";
var isMobile = script.getAttribute("data-mobile") === "true";
var wsBase = new URL(script.src).origin;
- Step 2: 新增
positionStylesMobile并切换pos
定位 positionStyles 定义(约第 16-21 行):
var positionStyles = {
"bottom-right": { bottom: "80px", right: "10px" },
"bottom-left": { bottom: "20px", left: "20px" },
"top-right": { top: "20px", right: "20px" },
"top-left": { top: "20px", left: "20px" },
};
在其下方紧接着新增一份移动端位置表:
var positionStylesMobile = {
"bottom-right": { bottom: "20px", right: "16px" },
"bottom-left": { bottom: "20px", left: "16px" },
"top-right": { top: "20px", right: "16px" },
"top-left": { top: "20px", left: "16px" },
};
然后定位 pos 计算(约第 29 行,紧接 var c = colors[theme] || colors.light; 之后):
var pos = positionStyles[position] || positionStyles["bottom-right"];
改为按 isMobile 选择表:
var pos = (isMobile ? positionStylesMobile : positionStyles)[position]
|| (isMobile ? positionStylesMobile : positionStyles)["bottom-right"];
目的:移动端圆钮仅 56px,沿用桌面
bottom-right的bottom:80px会离底边偏高;移动端改用贴角的 ~20px。桌面端positionStyles逐字不动。
- Step 3: 样式数组新增移动端 CSS
定位样式数组中 .toggle-btn:hover 那一行(约第 47 行):
".toggle-btn:hover { transform:scale(1.1); }",
在其下方紧接着插入 5 行移动端按钮样式:
".toggle-btn.mobile { flex-direction:column; gap:5px; padding:0; background:transparent; box-shadow:none; border-radius:0; }",
".toggle-btn.mobile:hover { transform:scale(1.05); }",
".toggle-btn.mobile .mobile-circle { position:relative; width:56px; height:56px; border-radius:50%; background:#f08a31; display:flex; align-items:center; justify-content:center; font-size:28px; box-shadow:0 4px 16px rgba(240,138,49,0.45); }",
".toggle-btn.mobile .mobile-dot { position:absolute; bottom:2px; right:2px; width:13px; height:13px; background:#22c55e; border-radius:50%; border:2px solid #f08a31; }",
".toggle-btn.mobile .mobile-label { color:#8C4D1D; font-size:10px; font-weight:600; line-height:1.1; }",
- Step 4:
toggleBtn改为按isMobile分两套模板
定位 toggleBtn 创建处(约第 233-235 行):
var toggleBtn = document.createElement("button");
toggleBtn.className = "toggle-btn";
toggleBtn.innerHTML = `
改动一:把 className 改为带 mobile 修饰:
toggleBtn.className = "toggle-btn" + (isMobile ? " mobile" : "");
改动二:把紧跟其后的单行 toggleBtn.innerHTML = \...桌面模板...`;改成if/else分支——在它**前面**插入移动端分支、把它本身包进else`。
具体:在 toggleBtn.className = ... 这一行之后、原 `toggleBtn.innerHTML = `` 这一行之前,插入:
if (isMobile) {
toggleBtn.innerHTML = `
<div class="mobile-circle">🤖<span class="mobile-dot"></span></div>
<span class="mobile-label">Chat with us</span>
`;
} else {
然后在原桌面模板赋值的结束反引号 `; 之后,补上闭合的 }:
}
改动后该区域结构应为(桌面模板内容逐字不变,仅被包进 else { ... }):
var toggleBtn = document.createElement("button");
toggleBtn.className = "toggle-btn" + (isMobile ? " mobile" : "");
if (isMobile) {
toggleBtn.innerHTML = `
<div class="mobile-circle">🤖<span class="mobile-dot"></span></div>
<span class="mobile-label">Chat with us</span>
`;
} else {
toggleBtn.innerHTML = `
<div style="position:relative;">
... (现有桌面模板,原样不动) ...
</div>
`;
}
toggleBtn仍是接收点击的元素,widget.js:367 的toggleBtn.addEventListener("click", toggle)无需改动,点击圆钮或文字均触发toggle()。
- Step 5: 手动验证——默认值(向后兼容,桌面款按钮不变)
widget-test.html 此刻未加 data-mobile → isMobile=false → 走桌面分支。
打开 static/widget-test.html(方式 A 或 B),DevTools Console 执行:
const host = document.querySelector('#cc-widget-host');
const btn = host.shadowRoot.querySelector('.toggle-btn');
console.log('class:', btn.className);
console.log('html:', btn.innerHTML.slice(0, 60));
Expected:
class:→toggle-btn(不含mobile)html:→ 以桌面模板开头(含position:relative的 div)
视觉上:右下角是现有的宽药丸按钮(🤖 图标 + "Chat with us" / "Usually replies quickly" 两行文字),与改动前一致 → 向后兼容验证通过。
- Step 6: 手动验证——
data-mobile="true"切到 D2(临时)
临时编辑 static/widget-test.html:69,给组件 script 标签加上 data-mobile="true"(保留 Step 1 Task 1 回退后去掉 data-auto-invite 的状态,即当前标签不含 auto-invite):
<script src="/static/widget.js" data-position="bottom-right" data-theme="light" data-whatsapp="https://wa.me/8613800138000" data-email="mailto:demo@hesu-auto.com" data-mobile="true"></script>
刷新页面,DevTools Console 执行:
const host = document.querySelector('#cc-widget-host');
const btn = host.shadowRoot.querySelector('.toggle-btn');
const container = host.shadowRoot.querySelector('.widget-container');
console.log('class:', btn.className);
console.log('circle:', !!btn.querySelector('.mobile-circle'), 'label:', btn.querySelector('.mobile-label')?.textContent);
console.log('position bottom/right:', getComputedStyle(container).bottom, getComputedStyle(container).right);
Expected:
class:→toggle-btn mobilecircle:→true,label:→Chat with usposition bottom/right:→20px/16px(贴角,非 桌面的80px)
视觉上:右下角是 D2 竖排按钮——56px 橙色圆钮 🤖 在上、"Chat with us" 文字外置在下(非橙色底),且更贴近屏幕右下角。点击圆钮或文字均能开关聊天面板。
验证通过后,保留 data-mobile="true" 不要回退——这正是 Task 3 要定稿的演示状态。
- Step 7: 手动验证——
data-position其它取值不报错(临时)
把 Step 6 标签里的 data-position 分别临时改成 bottom-left、top-right、top-left,各刷新一次。
Expected:D2 按钮出现在对应角,位置贴角(~20px),无 JS 报错(DevTools Console 无红色)。验证完恢复 data-position="bottom-right"。
- Step 8: 提交
git add static/widget.js
git commit -m "$(cat <<'EOF'
feat(widget): 新增 data-mobile 切换移动端竖排悬浮按钮(D2)
data-mobile="true" 时悬浮按钮改为竖排样式(56px 圆钮在上、"Chat with us"
文字外置在下),并切换到贴角位置表 positionStylesMobile。桌面款模板与
位置逻辑逐字不动,默认行为完全向后兼容。
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
Task 3: widget-test.html 演示属性 + 更新测试清单
Files:
-
Modify:
static/widget-test.html:69(确认data-mobile="true"演示属性到位,Task 2 Step 6 已加) -
Modify:
docs/test-checklist.md(补充手动验证条目) -
Step 1: 确认
widget-test.html演示属性到位
确认 static/widget-test.html:69 已是(Task 2 Step 6 改成的):
<script src="/static/widget.js" data-position="bottom-right" data-theme="light" data-whatsapp="https://wa.me/8613800138000" data-email="mailto:demo@hesu-auto.com" data-mobile="true"></script>
若之后被改动过,恢复成上面这样。
故意不加
data-auto-invite="false":widget-test.html 的核心用途就是测试邀请卡自动弹出(页面标题即 "Widget Invite Card Test Page"),加 false 会让邀请卡不弹、破坏其回归用途。data-auto-invite的关闭路径靠临时编辑验证(见测试清单)。
- Step 2: 更新测试清单文档
在 docs/test-checklist.md 末尾追加一节:
## 邀请卡自动弹出开关 + 移动端悬浮按钮(2026-06-23)
### data-auto-invite
- [ ] **默认(未传)**:组件邀请卡在 ~1.8s 后自动弹出,与历史一致。
- [ ] **`data-auto-invite="false"`**:邀请卡不再自动弹出;悬浮按钮照常显示,点击仍能打开聊天面板。
- [ ] **空值/非法值**:`data-auto-invite=""` 或 `data-auto-invite="0"` 等价于未传,仍自动弹。
- [ ] **与 data-mobile 独立**:`data-mobile="true" data-auto-invite="false"` 时竖排按钮 + 不弹邀请卡,互不影响。
### data-mobile
- [ ] **默认(未传)**:悬浮按钮为桌面款宽药丸(`toggle-btn`,不含 `mobile`),与历史一致。
- [ ] **`data-mobile="true"`**:悬浮按钮为 D2 竖排(`toggle-btn mobile`,含 `.mobile-circle` + `.mobile-label`="Chat with us"),点击圆钮或文字均能开关聊天面板。
- [ ] **移动端位置贴角**:`data-mobile="true"` + `data-position="bottom-right"` 时容器 `bottom/right` 为 `20px`/`16px`(非桌面 `80px`)。
- [ ] **位置覆盖各角**:`data-mobile="true"` 配合 `data-position` 取 `bottom-left`/`top-right`/`top-left`,按钮出现在对应角、贴角、无 JS 报错。
- [ ] **空值/非法值**:`data-mobile=""` 或 `data-mobile="yes"` 等价于未传,回退桌面款。
- Step 3: 提交
git add static/widget-test.html docs/test-checklist.md
git commit -m "$(cat <<'EOF'
docs(test): widget-test 演示 data-mobile + 补充两个开关测试清单
Co-Authored-By: Claude <noreply@anthropic.com>
EOF
)"
完成标准(Definition of Done)
static/widget.js读取data-auto-invite与data-mobile两个新属性;不传时默认行为与改动前完全一致(桌面款按钮 + 邀请卡自动弹)。data-auto-invite="false"时邀请卡不再自动弹出,悬浮按钮仍可用。data-mobile="true"时悬浮按钮为 D2 竖排样式,且贴角位置生效。- 桌面分支(按钮模板、
positionStyles)逐字未改,grep确认无意外改动。 - 3 个 commit 已提交,每个自包含、可独立验证。
docs/test-checklist.md已补充两个开关的手动验证条目。
风险与回滚
- 风险点:悬浮按钮是所有嵌入共享的代码路径,Task 2 改动它时若手滑可能影响桌面款。规避:桌面模板逐字包进
else、positionStyles不动;Task 2 Step 5 专门做桌面款回归验证。 - 回滚:
git revert对应 commit 即可;未传新属性的部署本来就无任何行为变化(零破坏)。