实现搜索引擎页面的悬浮按钮功能,支持拖动调整位置和点击打开高级搜索面板。新增浮动面板组件,包含完整的搜索表单和交互逻辑。优化URL解码和搜索引擎选择器配置,提升用户体验。 - 新增浮动面板组件及样式 - 实现悬浮按钮的拖动和点击交互 - 添加URL安全解码工具函数 - 配置搜索引擎选择器和注入位置 - 优化多语言支持和响应式布局 - 添加性能测试和单元测试
18 KiB
需求确认文档 - 改进型悬浮面板交互
📋 基本信息
- 功能名称: 改进型悬浮面板交互 (Improved Floating Panel)
- 功能代号:
improved-floating-panel - 目标版本: v1.7.3+
- 优先级: 高
- 创建时间: 2025-11-20
- 需求状态: ✅ 已确认 (质量评分: 94/100)
🎯 原始需求
用户请求: "请你实现方案 2"
方案2定义 (来自之前对话):
改进型悬浮面板 - 优化现有的悬浮按钮功能,启用并改进
FEATURE_FLAGS.enableFloatingButton,实现类似 Grammarly 的优雅交互体验。
🏗️ 仓库上下文分析
现有基础设施
项目类型: Chrome Extension (Manifest V3) 技术栈: React 18 + TypeScript 5 + Vite 5 当前版本: v1.7.2
相关现有代码
-
Content Script:
/src/content/index.ts- 已有
injectSearchFeatures()函数框架 - 功能开关:
FEATURE_FLAGS.enableFloatingButton = false(当前禁用) - 已有搜索引擎检测逻辑:
isSearchEnginePage()
- 已有
-
Popup 组件:
/src/popup/App.tsx- 完整的语法构建器UI
- React 组件化架构
- 支持 SearchForm、HistoryManager 等功能
-
消息通信:
/src/background/index.ts- 已有
chrome.runtime.onMessage监听器 - 支持
open_popup、quick_search等消息类型
- 已有
-
适配器系统:
/src/services/adapters/- 已支持 Baidu、Google、Bing 等10个搜索引擎
- 适配器工厂模式:
AdapterFactory.createAdapter(url)
集成约束
- Chrome 版本: Minimum Chrome 91 (manifest.json 定义)
- CSP 策略:
script-src 'self'; object-src 'self' - 国际化: 必须支持 zh-CN 和 en-US
- 主题: 必须支持 light/dark 模式
✅ 需求澄清过程
第一轮澄清 (2025-11-20)
问题组1: 交互体验细节
Q1.1: 内联图标的显示位置?
- ✅ 用户选择: C - 搜索框下方
- 决策说明: 作为提示/建议出现,不侵入搜索框本身
Q1.2: 图标触发方式?
- ✅ 用户选择: B - 焦点+悬停触发
- 决策说明: 焦点时显示,鼠标悬停在搜索区域也显示
Q1.3: 模态面板的尺寸和位置?
- ✅ 用户选择: B - 大面板 (800px × 600px)
- 决策说明: 提供充足空间展示完整语法构建器
Q1.4: 点击遮罩层的行为?
- ✅ 用户选择: A - 关闭面板
- 决策说明: 点击背景即关闭,符合标准模态框交互
问题组2: 技术实现选择
Q2.1: 面板渲染方式?
- ✅ 用户选择: A - Shadow DOM + iframe
- 决策说明: 完全隔离样式,避免与搜索引擎页面冲突
Q2.2: 如何复用现有 popup 组件?
- ✅ 用户选择: C - 消息传递方式
- 决策说明: content script 负责UI框架,业务逻辑通过消息传递
Q2.3: 搜索引擎 DOM 适配策略?
- ✅ 用户选择: A - 配置化适配
- 决策说明: 为每个搜索引擎定义选择器配置
问题组3: 边缘情况处理
Q3.1: DOM 结构更新处理?
- ✅ 用户选择: A - 自动重试
- 决策说明: 监听 DOM 变化,自动重新注入
Q3.2: 面板打开时焦点管理?
- ✅ 用户选择: A - 保持显示
- 决策说明: 面板打开后不受搜索框焦点影响
Q3.3: 快捷键冲突处理?
- ✅ 用户选择: C - 禁用快捷键
- 决策说明: 面板只能通过图标触发,避免与现有快捷键冲突
Q3.4: 多标签页处理?
- ✅ 用户选择: A - 独立状态
- 决策说明: 每个标签页的面板状态独立
问题组4: 功能范围确认
Q4.1: 功能开关策略?
- ✅ 用户选择: A - 默认启用
- 决策说明: 将
FEATURE_FLAGS.enableFloatingButton改为true
Q4.2: 与现有右键菜单的关系?
- ✅ 用户选择: A - 并存
- 决策说明: 两种入口都保留,用户自由选择
Q4.3: 支持的浏览器版本?
- ✅ 用户选择: A - Chrome 91+
- 决策说明: 与 manifest.json 保持一致
📐 详细功能规格
1. 用户交互流程
用户访问搜索引擎页面
↓
页面加载完成,content script 注入
↓
检测搜索框元素
↓
在搜索框下方注入触发图标 (初始隐藏)
↓
[触发条件1] 用户点击搜索框 (焦点事件)
[触发条件2] 鼠标悬停在搜索区域 (hover事件)
↓
图标从下方滑入/淡入显示
↓
用户点击图标
↓
显示模态遮罩层 (半透明黑色背景)
↓
在遮罩层上显示居中的面板 (800×600px)
↓
面板内通过 iframe 加载 popup/index.html
↓
用户在面板中构建搜索语法
↓
点击"应用"或"搜索"按钮
↓
通过消息传递将语法发送到 content script
↓
content script 将语法填充到搜索框
↓
关闭面板和遮罩层
↓
(可选) 自动触发搜索
2. 技术架构设计
2.1 组件结构
Content Script (src/content/index.ts)
├── FloatingPanelManager (新增)
│ ├── createTriggerIcon() // 创建触发图标
│ ├── createModalOverlay() // 创建遮罩层
│ ├── createPanelIframe() // 创建面板 iframe
│ ├── handleShow/Hide() // 显示/隐藏逻辑
│ └── handleMessage() // 消息通信
├── SearchEngineAdapter (扩展)
│ ├── getSearchInputSelector() // 获取搜索框选择器
│ ├── getInsertPosition() // 获取图标插入位置
│ └── fillSearchInput(query) // 填充搜索框
└── DOMObserver (新增)
├── observeSearchBox() // 监听搜索框变化
└── reInjectOnChange() // DOM变化时重新注入
2.2 搜索引擎适配配置
// src/config/search-engine-selectors.ts (新建)
interface SearchEngineConfig {
searchInputSelector: string;
searchContainerSelector: string;
iconInsertPosition: 'afterend' | 'beforeend';
iconOffsetY: string; // CSS position offset
}
const ENGINE_CONFIGS: Record<string, SearchEngineConfig> = {
baidu: {
searchInputSelector: 'input#kw',
searchContainerSelector: '#form',
iconInsertPosition: 'afterend',
iconOffsetY: '8px'
},
google: {
searchInputSelector: 'input[name="q"]',
searchContainerSelector: 'div.RNNXgb',
iconInsertPosition: 'afterend',
iconOffsetY: '8px'
},
bing: {
searchInputSelector: 'input#sb_form_q',
searchContainerSelector: 'form#sb_form',
iconInsertPosition: 'afterend',
iconOffsetY: '8px'
}
};
2.3 消息通信协议
// Content Script → Background → Popup
interface MessageProtocol {
// 请求打开面板 (content → background)
type: 'open_floating_panel';
// 应用搜索语法 (popup → content)
type: 'apply_search_syntax';
payload: {
query: string;
autoSearch: boolean;
};
// 关闭面板 (popup → content)
type: 'close_floating_panel';
}
3. UI/UX 规格
3.1 触发图标
位置: 搜索框下方,水平居中 尺寸: 120px × 32px 样式:
- 背景:
bg-blue-500(主题色) - 文字: "⚙️ 高级语法" (中文) / "⚙️ Advanced" (英文)
- 边框圆角:
rounded-md - 阴影:
shadow-md
动画:
- 显示: 从下方淡入 + 向上滑动 (
fadeInUp, 200ms) - 隐藏: 向下淡出 (
fadeOutDown, 200ms)
交互状态:
- Hover: 放大 1.05 倍 + 阴影加深
- Active: 按下效果 (scale 0.98)
3.2 模态遮罩层
样式:
- 背景:
rgba(0, 0, 0, 0.5)(半透明黑色) - z-index:
9999 - 全屏覆盖:
fixed top-0 left-0 w-full h-full
动画:
- 显示: 淡入 (
fadeIn, 200ms) - 隐藏: 淡出 (
fadeOut, 200ms)
交互:
- 点击遮罩 → 关闭面板
3.3 面板容器
尺寸: 800px × 600px 位置: 屏幕居中 样式:
- 背景: 白色 (light) / 深灰 (dark)
- 边框圆角:
rounded-lg - 阴影:
shadow-2xl
动画:
- 显示: 缩放弹出 (
zoomIn, 300ms, ease-out`) - 隐藏: 缩放消失 (
zoomOut, 200ms, ease-in`)
内容:
- iframe 加载
/src/popup/index.html - iframe 尺寸: 100% × 100%
- 无边框:
border: none
3.4 响应式适配
/* 小屏幕适配 (< 900px) */
@media (max-width: 900px) {
.ssp-panel {
width: 90vw;
height: 80vh;
max-width: 700px;
max-height: 600px;
}
}
/* 极小屏幕 (< 600px) */
@media (max-width: 600px) {
.ssp-panel {
width: 95vw;
height: 90vh;
}
}
4. 功能需求详细说明
4.1 自动 DOM 注入
触发时机:
- 页面加载完成 (
DOMContentLoaded) - 页面 URL 变化 (SPA 路由切换)
- DOM 结构变化 (MutationObserver)
注入逻辑:
- 检测当前页面是否为搜索引擎
- 使用配置化选择器定位搜索框
- 在搜索框下方插入触发图标
- 绑定焦点/悬停事件监听器
容错处理:
- 如果选择器未找到元素 → 延迟500ms重试 (最多3次)
- 如果搜索引擎未配置 → 静默失败,不注入
- 如果已注入 → 跳过,避免重复
4.2 焦点与悬停管理
焦点事件:
searchInput.addEventListener('focus', () => {
showTriggerIcon();
});
searchInput.addEventListener('blur', () => {
// 延迟隐藏,防止点击图标时过早消失
setTimeout(() => {
if (!isIconHovered && !isPanelOpen) {
hideTriggerIcon();
}
}, 200);
});
悬停事件:
searchContainer.addEventListener('mouseenter', () => {
showTriggerIcon();
});
searchContainer.addEventListener('mouseleave', () => {
if (!searchInput.matches(':focus') && !isPanelOpen) {
hideTriggerIcon();
}
});
4.3 面板生命周期
打开流程:
- 创建遮罩层元素 (如不存在)
- 创建面板容器 (如不存在)
- 创建 iframe 并加载 popup HTML
- 添加到 Shadow DOM 中
- 执行显示动画
- 设置
isPanelOpen = true
关闭流程:
- 执行隐藏动画
- 动画结束后移除遮罩层和面板
- 清理事件监听器
- 设置
isPanelOpen = false
消息通信:
- iframe 加载完成 → 发送初始化消息
- 用户点击"应用" → popup 发送
apply_search_syntax消息 - content script 接收 → 填充搜索框 → 关闭面板
4.4 搜索框填充
填充逻辑:
function fillSearchInput(query: string, autoSearch: boolean = false) {
const searchInput = getSearchInput();
// 设置值
searchInput.value = query;
// 触发 input 事件 (某些搜索引擎需要)
searchInput.dispatchEvent(new Event('input', { bubbles: true }));
// 触发 change 事件
searchInput.dispatchEvent(new Event('change', { bubbles: true }));
// 设置焦点
searchInput.focus();
// 可选: 自动提交搜索
if (autoSearch) {
const form = searchInput.closest('form');
if (form) {
form.submit();
}
}
}
4.5 多标签页独立状态
实现方式:
- 每个标签页的 content script 独立运行
- 不使用
chrome.storage共享面板状态 - 每个标签页的面板 DOM 元素独立创建/销毁
4.6 国际化支持
触发图标文本:
const iconText = getCurrentLanguage() === 'zh-CN'
? '⚙️ 高级语法'
: '⚙️ Advanced Syntax';
iframe 页面语言:
- iframe 加载的 popup 会自动读取
chrome.storage中的语言设置 - 无需额外处理
4.7 主题适配
Shadow DOM 样式:
/* 遮罩层根据系统主题自动适配 */
.ssp-modal-overlay {
background: rgba(0, 0, 0, 0.5);
}
/* 面板容器根据主题切换 */
.ssp-panel-container {
background: var(--bg-color);
color: var(--text-color);
}
/* 从 chrome.storage 读取主题设置并注入 CSS 变量 */
5. 边缘情况处理
5.1 DOM 结构变化
问题: 搜索引擎页面 DOM 更新导致图标消失
解决方案:
const observer = new MutationObserver((mutations) => {
const iconExists = document.querySelector('#ssp-trigger-icon');
if (!iconExists && isSearchEnginePage()) {
console.log('[SSP] DOM changed, re-injecting icon');
setTimeout(() => injectTriggerIcon(), 500);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
限流优化:
- 使用 debounce 避免频繁重注入
- 最多每500ms检查一次
5.2 iframe 加载失败
问题: iframe 无法加载 popup HTML
解决方案:
iframe.addEventListener('error', () => {
console.error('[SSP] Failed to load popup iframe');
showErrorMessage('面板加载失败,请刷新页面重试');
closePanelAfterDelay(3000);
});
iframe.addEventListener('load', () => {
console.log('[SSP] Popup iframe loaded successfully');
initMessageChannel();
});
5.3 CSP 策略阻止
问题: 某些搜索引擎的 CSP 策略阻止 iframe
解决方案:
- 使用
chrome.runtime.getURL()确保 iframe src 为扩展内部资源 - 在 manifest.json 的
web_accessible_resources中声明 popup HTML
{
"web_accessible_resources": [
{
"resources": ["src/popup/index.html", "icons/*"],
"matches": ["<all_urls>"]
}
]
}
5.4 快速点击防抖
问题: 用户快速连续点击图标导致多个面板
解决方案:
let isOpening = false;
function openPanel() {
if (isOpening || isPanelOpen) {
return;
}
isOpening = true;
// 执行打开逻辑...
setTimeout(() => {
isOpening = false;
}, 500);
}
5.5 焦点陷阱 (Focus Trap)
问题: 面板打开后,Tab 键可能跳出面板
解决方案:
// 在 iframe 内部的 popup 组件中实现焦点陷阱
modalElement.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
trapFocusInModal(e);
}
if (e.key === 'Escape') {
closePanel();
}
});
6. 性能优化
6.1 懒加载 iframe
策略: 只有在用户点击图标时才创建 iframe
let panelIframe: HTMLIFrameElement | null = null;
function openPanel() {
if (!panelIframe) {
panelIframe = createPanelIframe();
}
showModal();
}
6.2 CSS 动画硬件加速
优化:
.ssp-panel-container {
will-change: transform, opacity;
transform: translateZ(0); /* 触发 GPU 加速 */
}
6.3 事件委托
优化: 遮罩层点击事件使用事件委托
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
closePanel();
}
});
6.4 节流/防抖
应用场景:
- DOM MutationObserver → debounce 500ms
- 窗口 resize 事件 → throttle 200ms
- 鼠标 hover 事件 → debounce 100ms
🎯 需求质量评分 (最终)
基于详细的需求澄清和规格定义,重新评分:
| 评估维度 | 得分 | 说明 |
|---|---|---|
| 功能清晰度 (30分) | 28/30 | ✅ 交互流程、UI规格、动画效果已明确 |
| 技术具体性 (25分) | 24/25 | ✅ Shadow DOM架构、消息协议、适配配置已定义 |
| 实现完整性 (25分) | 24/25 | ✅ 边缘情况、错误处理、性能优化已覆盖 |
| 业务上下文 (20分) | 18/20 | ✅ 用户价值清晰,与现有功能协调明确 |
最终总分: 94/100 ✅
质量等级: A级 (优秀) - 可以进入实现阶段
📦 需求范围总结
包含功能 (In Scope)
✅ 在搜索框下方注入触发图标 ✅ 焦点+悬停双触发机制 ✅ 居中模态面板 (800×600px) ✅ Shadow DOM + iframe 渲染隔离 ✅ 消息传递复用 popup 组件 ✅ 配置化搜索引擎适配 ✅ 自动 DOM 变化重注入 ✅ 多标签页独立状态 ✅ 点击遮罩关闭面板 ✅ 国际化和主题支持 ✅ 默认启用功能开关 ✅ 与右键菜单并存 ✅ Chrome 91+ 兼容性
不包含功能 (Out of Scope)
❌ 侧边栏面板方式 (非本方案范围) ❌ 搜索框内部图标注入 (已选择下方位置) ❌ 用户自定义面板尺寸 (固定800×600) ❌ 面板位置拖拽 (固定居中) ❌ 快捷键打开面板 (仅图标触发) ❌ 多标签页状态共享 (已选独立状态) ❌ 设置页面功能开关 (直接默认启用)
未来可能扩展 (Future Enhancements)
🔮 支持更多搜索引擎适配 🔮 面板位置自定义 🔮 快捷键打开支持 🔮 面板尺寸用户可调 🔮 拖拽移动面板
🚀 实现优先级
P0 - 核心功能 (必须实现)
- 触发图标注入和显示/隐藏逻辑
- 模态遮罩层和面板容器
- iframe 加载和消息通信
- 搜索框填充功能
- 搜索引擎适配配置 (Baidu、Google、Bing)
P1 - 重要功能 (应该实现)
- DOM 变化自动重注入
- 焦点和悬停事件管理
- 动画效果和过渡
- 错误处理和容错
- 国际化和主题适配
P2 - 优化功能 (可以实现)
- 性能优化 (懒加载、硬件加速)
- 响应式布局适配
- 焦点陷阱和键盘导航
- 更多搜索引擎支持
📝 验收标准
功能验收
- 在 Baidu.com 搜索框下方能看到触发图标
- 在 Google.com 搜索框下方能看到触发图标
- 在 Bing.com 搜索框下方能看到触发图标
- 搜索框获得焦点时,图标自动显示
- 鼠标悬停在搜索区域时,图标自动显示
- 失去焦点且无悬停时,图标自动隐藏
- 点击图标后,显示居中的800×600模态面板
- 面板内正确加载 popup 组件
- 在面板中构建语法后,点击"应用"能填充到搜索框
- 点击遮罩层背景能关闭面板
- 按 ESC 键能关闭面板
- 面板打开后不受搜索框焦点影响
技术验收
- 使用 Shadow DOM 隔离样式
- 使用 iframe 加载 popup HTML
- 通过消息传递与 popup 通信
- 配置化适配器支持多搜索引擎
- MutationObserver 监听 DOM 变化
- 自动重注入功能正常工作
- 多个标签页独立运行,互不干扰
- 中英文界面正确显示
- 亮色/暗色主题正确适配
- 在 Chrome 91+ 版本正常运行
兼容性验收
- Chrome 91 浏览器测试通过
- Chrome 最新版本测试通过
- Windows 系统测试通过
- macOS 系统测试通过
- Linux 系统测试通过 (可选)
性能验收
- 图标显示/隐藏动画流畅 (>60fps)
- 面板打开/关闭动画流畅 (>60fps)
- 页面加载时注入不阻塞主线程
- 内存占用合理 (<10MB 增量)
- 不影响搜索引擎页面原有功能
🔗 相关文档
✍️ 变更历史
| 版本 | 日期 | 变更内容 | 负责人 |
|---|---|---|---|
| v1.0 | 2025-11-20 | 初始版本,完成需求澄清 | requirements-pilot |
文档状态: ✅ 已确认,待用户审批进入实现阶段