Files
ai-kefu-jc/docs/superpowers/specs/2026-06-23-auto-invite-toggle-and-mobile-button-design.md
T
2026-09-29 14:15:12 +08:00

216 lines
12 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.
# 邀请卡自动弹出开关 + 移动端悬浮按钮配置
**日期**: 2026-06-23
**项目**: AI 客服系统 - VOLT/HESU
**目标**: 为嵌入式组件(widget.js)新增两个 `data-*` 配置开关——控制「邀请卡是否自动弹出」、以及「是否使用移动端悬浮按钮样式」,并让移动端按钮采用「图标在上、文字在下」的竖排样式(D2)。
---
## 一、设计概述
### 1.1 项目背景
当前嵌入式组件 [static/widget.js](../../../static/widget.js) 有两处行为是写死、无法按部署点配置的:
1. **邀请卡固定 1.8s 后自动弹出**:[widget.js:306-316](../../../static/widget.js#L306-L316) 的 `startInviteTimer()` 无条件启动,所有嵌入站点都会弹。部分站点不希望主动打扰用户,但目前无法关闭。
2. **悬浮按钮只有桌面款一种样式**:[widget.js:233-244](../../../static/widget.js#L233-L244) 的 `toggle-btn` 是一个「图标在左、文字在右」的宽药丸。在移动端窄屏上偏宽、略挡内容。
本次复用上一轮「联系方式动态配置」已建立的 `data-*` 配置范式(见 [2026-06-18-dynamic-contact-config-design.md](./2026-06-18-dynamic-contact-config-design.md)),把上述两处行为也变成可配置。
### 1.2 设计目标
- 接入方可通过 `data-auto-invite` 控制邀请卡是否自动弹出:默认弹(保持现状),设 `false` 则不弹。
- 接入方可通过 `data-mobile` 切换悬浮按钮到移动端竖排样式(D2:圆钮在上、文字外置在下)。
- **完全向后兼容**:不传任何新属性时,行为与现在线上完全一致。
- 复用已有 `data-*` 机制,不引入新的配置范式。
### 1.3 非目标(Out of Scope)
- `data-mobile` **只影响悬浮按钮样式**,不改聊天面板、不改邀请卡。聊天面板已有自己的 `@media(max-width:450px)` 响应式([widget.js:152](../../../static/widget.js#L152)),各管各的,本次不统一。
- **不做自动检测**:移动端判定采用纯手动标识(方案 A),不引入 UA 嗅探、不读 `window.innerWidth`。接入方按部署点自行打标。
- **独立聊天页(chat.html / chat.js)不在本次范围**:独立页没有邀请卡逻辑,悬浮按钮也是 widget 独有的概念。本次只改 widget.js。
- 后端、admin、配置文件均不动。
---
## 二、配置约定(契约)
### 2.1 属性定义
| 属性 | 含义 | 取值 | 默认值(未提供时) |
|------|------|------|--------------------|
| `data-auto-invite` | 邀请卡是否在 1.8s 后自动弹出 | `"false"` 关闭;其它/缺省为开启 | 开启(保持现状) |
| `data-mobile` | 是否使用移动端竖排悬浮按钮(D2) | `"true"` 启用;其它/缺省为桌面款 | `false`(桌面款) |
### 2.2 读取语义
沿用 widget.js 现有的字符串判定写法(与 [widget.js:11](../../../static/widget.js#L11) 的 `data-auto-open` 一致):
```js
var autoInvite = script.getAttribute("data-auto-invite") !== "false"; // 默认 true
var isMobile = script.getAttribute("data-mobile") === "true"; // 默认 false
```
- `data-auto-invite` 采用**负向逻辑**(设 `false` 关闭),是为了**保持现有默认行为不变**——现在线上就是会弹,缺省必须仍是「弹」。这与 `data-auto-open`(设 `true` 才开)方向相反,但两者默认状态不同,不冲突。
- 属性缺失、空字符串、或非 `"false"`/`"true"` 的任意值:`autoInvite` 视为 `true`、`isMobile` 视为 `false`,即回退默认。
### 2.3 接入示例
```html
<!-- 桌面端、保留邀请卡自动弹(什么都不加,现状) -->
<script src="https://chat.example.com/static/widget.js"></script>
<!-- 移动端站点:竖排按钮 + 关闭邀请卡自动弹 -->
<script
src="https://chat.example.com/static/widget.js"
data-mobile="true"
data-auto-invite="false"
></script>
<!-- 仅关闭邀请卡自动弹,按钮仍为桌面款 -->
<script
src="https://chat.example.com/static/widget.js"
data-auto-invite="false"
></script>
```
两个开关互相独立,可任意组合。
---
## 三、改动详情
全部改动集中在 [static/widget.js](../../../static/widget.js) 与测试页 [static/widget-test.html](../../../static/widget-test.html)。
### 3.1 `static/widget.js` — 读取两个新属性
在 [widget.js:8-14](../../../static/widget.js#L8-L14) 现有 `getAttribute` 调用旁,新增两行:
```js
var autoInvite = script.getAttribute("data-auto-invite") !== "false";
var isMobile = script.getAttribute("data-mobile") === "true";
```
### 3.2 `static/widget.js` — 邀请卡自动弹出开关
在 [widget.js:306](../../../static/widget.js#L306) 的 `startInviteTimer()` 函数**最开头**加一句短路:
```js
function startInviteTimer() {
if (!autoInvite) return; // ← 新增:配置关闭则不启动计时器
if (checkInviteStorage()) {
return; // 已被永久关闭
}
inviteTimer = window.setTimeout(function () { ... }, 1800);
}
```
效果:`data-auto-invite="false"` 时计时器根本不启动,邀请卡永不自动弹出。点悬浮按钮仍照常打开聊天面板;邀请卡内部的 "Start Chat" / "No Thanks" 等逻辑因卡片不出现而自然不触发,无需额外改动。
### 3.3 `static/widget.js` — 移动端悬浮按钮(D2)
悬浮按钮当前在 [widget.js:233-244](../../../static/widget.js#L233-L244) 一处生成,`toggleBtn.innerHTML` 为桌面款模板。改动为**按 `isMobile` 分两套模板**,点击事件不变。
**实现路径选择(见第四节):采用「独立移动端 DOM 模板」**,桌面款模板逐字不动搬入 `else` 分支。
**DOM 结构(D2,竖排:圆钮在上、文字在外在下)**:
```js
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 = `/* 现有桌面款模板(widget.js:235-244),原样照搬、逐字不动 */`;
}
```
`toggleBtn` 仍是接收点击的元素([widget.js:367](../../../static/widget.js#L367) 的 `toggleBtn.addEventListener("click", toggle)` 不变),点击圆钮或文字均触发 `toggle()`。
**新增 CSS(追加到 [widget.js:40](../../../static/widget.js#L40) 注入的样式数组中)**:
```js
// 移动端竖排按钮(D2):覆盖桌面款药丸背景,把橙色挪到圆钮上
".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; }",
```
> **位置**:桌面端位置逻辑不变——按钮仍位于由 `positionStyles` 决定的容器锚点(默认右下)。移动端采用更贴角的位置表(见下「位置偏移」)。
>
> **颜色与主题**:移动端按钮颜色硬编码固定橙色(`#f08a31` 圆钮 + `#8C4D1D` 文字),**不随 `data-theme` 切换**——这与现有桌面款按钮(同样固定橙色、忽略 `data-theme`)行为一致,实现时无需特殊处理主题。
**位置偏移(移动端贴角)**:
现有 `positionStyles`([widget.js:16-21](../../../static/widget.js#L16-L21))的 `bottom-right` 取 `bottom:80px`(为高药丸按钮预留)。移动端圆钮仅 56px,沿用 80px 会在手机上离底边偏高、显得"飘"。故 `isMobile` 时改用一份更贴角的位置表 `positionStylesMobile`:
```js
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" },
};
var pos = (isMobile ? positionStylesMobile : positionStyles)[position]
|| (isMobile ? positionStylesMobile : positionStyles)["bottom-right"];
```
桌面端 `positionStyles` 逐字不动;仅当 `data-mobile="true"` 时切换到 `positionStylesMobile`。聊天面板/邀请卡因相对容器定位(`bottom:80px`),随容器下移后仍开在按钮上方,不重叠。
### 3.4 `static/widget-test.html` — 演示 / 测试属性
在 [widget-test.html:69](../../../static/widget-test.html#L69) 的组件 `<script>` 标签上,新增 `data-mobile` / `data-auto-invite` 属性以便本地验证;或保留默认、另起一个测试用例覆盖移动端。具体由实现阶段在测试清单中明确。
---
## 四、实现路径:独立移动端 DOM 模板 vs CSS 类切换
移动端按钮(D2)与桌面款 DOM 结构差异较大:桌面是「图标+文字都在一个橙色药丸里」,D2 是「圆钮在上、文字在按钮外面」。两种实现法:
| 方案 | 做法 | 取舍 |
|------|------|------|
| **① 独立移动端 DOM 模板(采用)** | `isMobile` 时拼一段不同的 `innerHTML`,桌面模板原样搬进 `else` | 结构清晰、互不干扰;改一边不影响另一边;与「桌面分支逐字不变」的兼容性目标天然吻合 |
| ② 同一 DOM + CSS 类切换 | 保留一份结构,加 `.mobile` class 用 CSS 把文字挪到按钮外 | 代码略少,但要硬掰布局、容易出现重叠/错位,调试成本高 |
**采用 ①**:D2 结构差异够大,独立模板最干净,且桌面分支零改动 = 现有嵌入零风险。
---
## 五、向后兼容性
- **不传任何新属性**:`autoInvite=true`、`isMobile=false`。
- 邀请卡仍 1.8s 自动弹(`startInviteTimer` 正常执行)。
- 悬浮按钮仍走桌面款模板(`toggle-btn`,无 `.mobile` class,样式与现状完全一致)。
- → 现有所有嵌入脚本(均未写这两个新属性)行为**与线上完全一致**。
- 不删除、不修改任何现有 `data-*` 配置(`data-position` / `data-theme` / `data-title` / `data-auto-open` / `data-whatsapp` / `data-email`)。
- **唯一风险点**:悬浮按钮那段是所有嵌入共享的代码路径。规避方式 = 桌面分支逐字不动、移动 D2 完全独立成分支(见第四节)。
- 邀请卡改动仅为函数开头加一句 `if (!autoInvite) return;`,极小、极孤立。
---
## 六、验收 / 测试
本仓库为纯静态 JS,无自动化测试框架,采用**手动测试清单**(建议补充到 [docs/test-checklist.md](../../test-checklist.md)):
1. **默认值回归(不传新属性)**:用 widget-test.html 验证——邀请卡仍 1.8s 后弹出;悬浮按钮仍为桌面款宽药丸;点击按钮正常开关聊天面板。与改动前逐项一致。
2. **`data-auto-invite="false"`**:邀请卡不再自动弹出;悬浮按钮(桌面款)点击仍能打开聊天面板;聊天面板内交互正常。
3. **`data-mobile="true"`**:悬浮按钮变为 D2 竖排样式(56px 圆钮在上、"Chat with us" 文字外置在下、非橙色底);点击圆钮或文字均能开关聊天面板;邀请卡仍按默认自动弹(两者独立)。
4. **组合 `data-mobile="true" data-auto-invite="false"`**:竖排按钮 + 不弹邀请卡,两者互不影响。
5. **空值/非法值**:`data-mobile=""` 或 `data-mobile="yes"` 等价于未传(回退桌面款);`data-auto-invite="0"` 等价于未传(仍弹)。
6. **位置配置**:`data-mobile="true"` 配合 `data-position` 各取值,按钮锚点位置正确;尤其验证 `bottom-right` 下移动端按钮贴角(约 `bottom:20px`)而非沿用桌面 `80px`。
---
## 七、影响文件清单
| 文件 | 改动类型 |
|------|----------|
| `static/widget.js` | 读取 2 个新属性 + 邀请卡短路 1 行 + 悬浮按钮分两套模板 + 新增移动端 CSS |
| `static/widget-test.html` | 组件 script 标签加 `data-mobile` / `data-auto-invite`(演示/测试用) |