49c0e5559c
Co-Authored-By: Claude Code <noreply@anthropic.com>
216 lines
12 KiB
Markdown
216 lines
12 KiB
Markdown
# 邀请卡自动弹出开关 + 移动端悬浮按钮配置
|
||
|
||
**日期**: 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`(演示/测试用) |
|