# 邀请卡自动弹出开关 + 移动端悬浮按钮配置 **日期**: 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 ``` 两个开关互相独立,可任意组合。 --- ## 三、改动详情 全部改动集中在 [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 = `
🤖
Chat with us `; } 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) 的组件 `