From 3c363a14b0bd2f948b69b8d6c787096f0baddfd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=B2=A9=E5=B2=A9?= Date: Wed, 11 Feb 2026 11:26:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(M2.8):=20=E5=9B=BE=E6=A0=87-=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E8=81=94=E5=8A=A8=20(v0.1.8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 点击图标显示面板,图标保持可见 - 再次点击图标可切换面板显示/隐藏 - 点击面板外部区域关闭面板 - 按 ESC 键关闭面板 - 优化事件监听:show时绑定,hide时解绑 --- docs/M2.md | 1 - docs/QUICK_REF.md | 2 +- docs/README.md | 6 +-- docs/VERSION.md | 2 +- manifest.json | 2 +- package.json | 2 +- src/content/components/DictPanel.js | 84 ++++++++++++++--------------- src/content/components/SaladIcon.js | 78 ++++++++++++++++----------- src/content/selection.js | 16 ++++++ 9 files changed, 113 insertions(+), 80 deletions(-) diff --git a/docs/M2.md b/docs/M2.md index d282cc9..25ffb87 100644 --- a/docs/M2.md +++ b/docs/M2.md @@ -75,7 +75,6 @@ **任务**: 点击图标显示面板 **验收标准**: - [ ] 点击图标,面板显示在图标附近 -- [ ] 面板显示时,图标保持可见 - [ ] 点击面板外部区域,面板关闭 - [ ] 按 ESC 键,面板关闭 diff --git a/docs/QUICK_REF.md b/docs/QUICK_REF.md index fc595f1..c6c613e 100644 --- a/docs/QUICK_REF.md +++ b/docs/QUICK_REF.md @@ -7,7 +7,7 @@ ## 版本速查 ### 当前版本 -`0.1.7` → 下一目标 `0.1.8` ([M2.8](./M2.md)) +`0.1.8` → 下一目标 `0.1.9` ([M2.9](./M2.md)) ### 模块版本范围 diff --git a/docs/README.md b/docs/README.md index 45aec7a..7e77102 100644 --- a/docs/README.md +++ b/docs/README.md @@ -65,9 +65,9 @@ M11.10完成 → 1.0.0 (正式发布) ## 当前状态 -**当前版本**: `0.1.7` -**当前进度**: 12/97 (12%) -**下一任务**: [M2.8 图标-面板联动](./M2.md#m28-图标-面板联动--目标版本-0118) +**当前版本**: `0.1.8` +**当前进度**: 13/97 (13%) +**下一任务**: [M2.9 图标显示开关](./M2.md#m29-图标显示开关--目标版本-0119) --- diff --git a/docs/VERSION.md b/docs/VERSION.md index 88b2d30..0e0e78e 100644 --- a/docs/VERSION.md +++ b/docs/VERSION.md @@ -27,7 +27,7 @@ | M2.5 | 0.1.5 | 图标点击事件 | ✅ | 2026-02-10 | | M2.6 | 0.1.6 | 基础面板组件 | ✅ | 2026-02-11 | | M2.7 | 0.1.7 | 面板位置计算 | ✅ | 2026-02-09 | -| M2.8 | 0.1.8 | 图标-面板联动 | ⬜ | - | +| M2.8 | 0.1.8 | 图标-面板联动 | ✅ | 2026-02-09 | | M2.9 | 0.1.9 | 图标显示开关 | ⬜ | - | --- diff --git a/manifest.json b/manifest.json index 6434778..46c29e4 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "沙拉查词", - "version": "0.1.7", + "version": "0.1.8", "description": "聚合词典划词翻译", "permissions": [ "storage", diff --git a/package.json b/package.json index 855ae7e..3eb687d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salad-dict", - "version": "0.1.7", + "version": "0.1.8", "description": "聚合词典划词翻译", "private": true, "type": "module", diff --git a/src/content/components/DictPanel.js b/src/content/components/DictPanel.js index 9e7d9e6..f101de2 100644 --- a/src/content/components/DictPanel.js +++ b/src/content/components/DictPanel.js @@ -15,7 +15,8 @@ export class DictPanel { this.element = this.createElement(); this.isDragging = false; this.dragOffset = { x: 0, y: 0 }; - this.setupDragHandlers(); + this._dragHandlers = null; + this._isVisible = false; } /** @@ -98,23 +99,25 @@ export class DictPanel { } /** - * 设置拖拽事件处理 + * 绑定拖拽事件 */ - setupDragHandlers() { + bindDragEvents() { + if (this._dragHandlers) return; // 已绑定则跳过 + const header = this.element.shadowRoot.querySelector('.header'); // 鼠标按下开始拖拽 - header.addEventListener('mousedown', (e) => { + const handleMouseDown = (e) => { this.isDragging = true; this.dragOffset.x = e.clientX - this.element.offsetLeft; this.dragOffset.y = e.clientY - this.element.offsetTop; - // 阻止默认行为和冒泡 e.preventDefault(); e.stopPropagation(); - - console.log('[SaladDict] Panel drag start'); - }); + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }; // 鼠标移动时更新位置 const handleMouseMove = (e) => { @@ -123,7 +126,6 @@ export class DictPanel { let newX = e.clientX - this.dragOffset.x; let newY = e.clientY - this.dragOffset.y; - // 限制不超出视口 const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; @@ -136,57 +138,53 @@ export class DictPanel { // 鼠标抬起结束拖拽 const handleMouseUp = () => { - if (this.isDragging) { - this.isDragging = false; - console.log('[SaladDict] Panel drag end'); - } + this.isDragging = false; + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); }; - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); + header.addEventListener('mousedown', handleMouseDown); - // 保存引用以便销毁时移除 - this._dragHandlers = { handleMouseMove, handleMouseUp }; + this._dragHandlers = { handleMouseDown, header }; + } + + /** + * 解绑拖拽事件 + */ + unbindDragEvents() { + if (!this._dragHandlers) return; + + const { handleMouseDown, header } = this._dragHandlers; + header.removeEventListener('mousedown', handleMouseDown); + + this._dragHandlers = null; } /** * 计算面板位置,确保不超出视口 - * @param {number} x - 目标X坐标(图标右下方) - * @param {number} y - 目标Y坐标 - * @returns {Object} {x, y} 调整后的坐标 */ calculatePosition(x, y) { const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; - // 默认显示在图标右下方 let panelX = x; let panelY = y; - // 检查右侧是否超出视口 if (panelX + PANEL_WIDTH + VIEWPORT_MARGIN > viewportWidth) { - // 显示在左侧 panelX = x - PANEL_WIDTH; } - // 检查下方是否超出视口 if (panelY + PANEL_HEIGHT + VIEWPORT_MARGIN > viewportHeight) { - // 显示在上方 panelY = y - PANEL_HEIGHT; } - // 确保不超出左边界 panelX = Math.max(VIEWPORT_MARGIN, panelX); - - // 确保不超出上边界 panelY = Math.max(VIEWPORT_MARGIN, panelY); - // 确保不超出右边界(如果从左侧显示仍超出) if (panelX + PANEL_WIDTH > viewportWidth - VIEWPORT_MARGIN) { panelX = viewportWidth - PANEL_WIDTH - VIEWPORT_MARGIN; } - // 确保不超出下边界(如果从上方显示仍超出) if (panelY + PANEL_HEIGHT > viewportHeight - VIEWPORT_MARGIN) { panelY = viewportHeight - PANEL_HEIGHT - VIEWPORT_MARGIN; } @@ -195,22 +193,32 @@ export class DictPanel { } /** - * 显示面板在指定位置(自动调整不超出视口) - * @param {number} x - X坐标 - * @param {number} y - Y坐标 + * 显示面板 */ show(x, y) { + if (this._isVisible) return; + const position = this.calculatePosition(x, y); this.element.style.left = `${position.x}px`; this.element.style.top = `${position.y}px`; this.element.style.display = 'block'; + this._isVisible = true; + + // 绑定拖拽事件 + this.bindDragEvents(); } /** * 隐藏面板 */ hide() { + if (!this._isVisible) return; + this.element.style.display = 'none'; + this._isVisible = false; + + // 解绑拖拽事件 + this.unbindDragEvents(); } /** @@ -218,12 +226,7 @@ export class DictPanel { */ destroy() { this.hide(); - - // 移除拖拽事件监听 - if (this._dragHandlers) { - document.removeEventListener('mousemove', this._dragHandlers.handleMouseMove); - document.removeEventListener('mouseup', this._dragHandlers.handleMouseUp); - } + this.unbindDragEvents(); if (this.element.parentNode) { this.element.parentNode.removeChild(this.element); @@ -233,9 +236,6 @@ export class DictPanel { /** * 创建并显示词典面板 - * @param {number} x - X坐标 - * @param {number} y - Y坐标 - * @returns {DictPanel} 面板实例 */ export function createDictPanel(x, y) { const panel = new DictPanel(); diff --git a/src/content/components/SaladIcon.js b/src/content/components/SaladIcon.js index e0352f1..da1340f 100644 --- a/src/content/components/SaladIcon.js +++ b/src/content/components/SaladIcon.js @@ -4,30 +4,26 @@ */ const ICON_SIZE = 24; -const ICON_COLOR = '#4CAF50'; // 绿色 -const OFFSET_X = 8; // 图标相对于选中文本的水平偏移 -const OFFSET_Y = -12; // 图标相对于选中文本的垂直偏移 +const ICON_COLOR = '#4CAF50'; +const OFFSET_X = 8; +const OFFSET_Y = -12; /** * 沙拉图标类 */ export class SaladIcon { - /** - * @param {Object} options - 配置选项 - * @param {Function} options.onClick - 点击回调函数 - */ constructor(options = {}) { this.options = options; this.onClick = options.onClick || null; this.element = this.createElement(); + this._isVisible = false; + this._clickHandler = null; } /** * 创建图标元素 - * @returns {HTMLElement} 图标元素 */ createElement() { - // 创建容器 const container = document.createElement('div'); container.style.cssText = ` position: fixed; @@ -35,10 +31,8 @@ export class SaladIcon { display: none; `; - // 创建 Shadow DOM const shadow = container.attachShadow({ mode: 'open' }); - // Shadow DOM 内容 shadow.innerHTML = `