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

12 KiB
Raw Blame History

邀请卡自动弹出开关 + 移动端悬浮按钮配置

日期: 2026-06-23 项目: AI 客服系统 - VOLT/HESU 目标: 为嵌入式组件(widget.js)新增两个 data-* 配置开关——控制「邀请卡是否自动弹出」、以及「是否使用移动端悬浮按钮样式」,并让移动端按钮采用「图标在上、文字在下」的竖排样式(D2)。


一、设计概述

1.1 项目背景

当前嵌入式组件 static/widget.js 有两处行为是写死、无法按部署点配置的:

  1. 邀请卡固定 1.8s 后自动弹出:widget.js:306-316 的 startInviteTimer() 无条件启动,所有嵌入站点都会弹。部分站点不希望主动打扰用户,但目前无法关闭。
  2. 悬浮按钮只有桌面款一种样式:widget.js:233-244 的 toggle-btn 是一个「图标在左、文字在右」的宽药丸。在移动端窄屏上偏宽、略挡内容。

本次复用上一轮「联系方式动态配置」已建立的 data-* 配置范式(见 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),各管各的,本次不统一。
  • 不做自动检测:移动端判定采用纯手动标识(方案 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 的 data-auto-open 一致):

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 接入示例

<!-- 桌面端、保留邀请卡自动弹(什么都不加,现状) -->
<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-test.html。

3.1 static/widget.js — 读取两个新属性

在 widget.js:8-14 现有 getAttribute 调用旁,新增两行:

var autoInvite = script.getAttribute("data-auto-invite") !== "false";
var isMobile   = script.getAttribute("data-mobile") === "true";

3.2 static/widget.js — 邀请卡自动弹出开关

在 widget.js:306 的 startInviteTimer() 函数最开头加一句短路:

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 一处生成,toggleBtn.innerHTML 为桌面款模板。改动为按 isMobile 分两套模板,点击事件不变。

实现路径选择(见第四节):采用「独立移动端 DOM 模板」,桌面款模板逐字不动搬入 else 分支。

DOM 结构(D2,竖排:圆钮在上、文字在外在下):

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 的 toggleBtn.addEventListener("click", toggle) 不变),点击圆钮或文字均触发 toggle()。

新增 CSS(追加到 widget.js:40 注入的样式数组中):

// 移动端竖排按钮(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)的 bottom-right 取 bottom:80px(为高药丸按钮预留)。移动端圆钮仅 56px,沿用 80px 会在手机上离底边偏高、显得"飘"。故 isMobile 时改用一份更贴角的位置表 positionStylesMobile:

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 的组件 <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):

  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(演示/测试用)