Compare commits
5 Commits
23a0e55979
...
4e12552437
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e12552437 | ||
|
|
aca09ccf25 | ||
|
|
314d9d47f8 | ||
|
|
b789e5255c | ||
|
|
d5639d2399 |
@ -7,7 +7,7 @@
|
||||
## 版本速查
|
||||
|
||||
### 当前版本
|
||||
`0.0.5` → 下一目标 `0.1.1` ([M2.1](./M2.md))
|
||||
`0.1.5` → 下一目标 `0.1.6` ([M2.6](./M2.md))
|
||||
|
||||
### 模块版本范围
|
||||
|
||||
|
||||
@ -65,9 +65,9 @@ M11.10完成 → 1.0.0 (正式发布)
|
||||
|
||||
## 当前状态
|
||||
|
||||
**当前版本**: `0.0.5`
|
||||
**当前进度**: 5/97 (5%)
|
||||
**下一任务**: [M2.1 文本选择检测](./M2.md#m21-文本选择检测--目标版本-011)
|
||||
**当前版本**: `0.1.5`
|
||||
**当前进度**: 10/97 (10%)
|
||||
**下一任务**: [M2.6 基础面板组件](./M2.md#m26-基础面板组件--目标版本-0116)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -29,11 +29,11 @@
|
||||
|
||||
| 任务 | 版本 | 描述 | 状态 | 日期 |
|
||||
|------|------|------|------|------|
|
||||
| M2.1 | 0.1.1 | 文本选择检测 | ⬜ | - |
|
||||
| M2.2 | 0.1.2 | 获取选中文本坐标 | ⬜ | - |
|
||||
| M2.3 | 0.1.3 | 沙拉图标组件 | ⬜ | - |
|
||||
| M2.4 | 0.1.4 | 图标定位显示 | ⬜ | - |
|
||||
| M2.5 | 0.1.5 | 图标点击事件 | ⬜ | - |
|
||||
| M2.1 | 0.1.1 | 文本选择检测 | ✅ | 2026-02-09 |
|
||||
| M2.2 | 0.1.2 | 获取选中文本坐标 | ✅ | 2026-02-09 |
|
||||
| M2.3 | 0.1.3 | 沙拉图标组件 | ✅ | 2026-02-09 |
|
||||
| M2.4 | 0.1.4 | 图标定位显示 | ✅ | 2026-02-09 |
|
||||
| M2.5 | 0.1.5 | 图标点击事件 | ✅ | 2026-02-09 |
|
||||
| M2.6 | 0.1.6 | 基础面板组件 | ⬜ | - |
|
||||
| M2.7 | 0.1.7 | 面板位置计算 | ⬜ | - |
|
||||
| M2.8 | 0.1.8 | 图标-面板联动 | ⬜ | - |
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "沙拉查词",
|
||||
"version": "0.0.5",
|
||||
"version": "0.1.5",
|
||||
"description": "聚合词典划词翻译",
|
||||
"permissions": [
|
||||
"storage",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "salad-dict",
|
||||
"version": "0.0.5",
|
||||
"version": "0.1.5",
|
||||
"description": "聚合词典划词翻译",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
135
src/content/components/SaladIcon.js
Normal file
135
src/content/components/SaladIcon.js
Normal file
@ -0,0 +1,135 @@
|
||||
/**
|
||||
* @file 沙拉图标组件
|
||||
* @description 在选中文本旁显示浮动图标
|
||||
*/
|
||||
|
||||
const ICON_SIZE = 24;
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建图标元素
|
||||
* @returns {HTMLElement} 图标元素
|
||||
*/
|
||||
createElement() {
|
||||
// 创建容器
|
||||
const container = document.createElement('div');
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 2147483647;
|
||||
display: none;
|
||||
`;
|
||||
|
||||
// 创建 Shadow DOM
|
||||
const shadow = container.attachShadow({ mode: 'open' });
|
||||
|
||||
// Shadow DOM 内容
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
.icon {
|
||||
width: ${ICON_SIZE}px;
|
||||
height: ${ICON_SIZE}px;
|
||||
background-color: ${ICON_COLOR};
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
transition: transform 0.1s ease, box-shadow 0.1s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.icon:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.salad-symbol {
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
<div class="icon">
|
||||
<span class="salad-symbol">S</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 处理图标点击
|
||||
container.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
console.log('[SaladDict] Icon clicked');
|
||||
|
||||
if (this.onClick) {
|
||||
this.onClick(e);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加到页面
|
||||
document.body.appendChild(container);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示图标在指定位置
|
||||
* @param {number} x - X坐标
|
||||
* @param {number} y - Y坐标
|
||||
*/
|
||||
show(x, y) {
|
||||
this.element.style.left = `${x + OFFSET_X}px`;
|
||||
this.element.style.top = `${y + OFFSET_Y}px`;
|
||||
this.element.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏图标
|
||||
*/
|
||||
hide() {
|
||||
this.element.style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁组件
|
||||
*/
|
||||
destroy() {
|
||||
this.hide();
|
||||
if (this.element.parentNode) {
|
||||
this.element.parentNode.removeChild(this.element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并显示沙拉图标
|
||||
* @param {number} x - X坐标
|
||||
* @param {number} y - Y坐标
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {Function} options.onClick - 点击回调函数
|
||||
* @returns {SaladIcon} 图标实例
|
||||
*/
|
||||
export function createSaladIcon(x, y, options = {}) {
|
||||
const icon = new SaladIcon(options);
|
||||
icon.show(x, y);
|
||||
return icon;
|
||||
}
|
||||
@ -1,31 +1,34 @@
|
||||
// Content Script
|
||||
import { messaging, ping } from '../shared/messaging.js';
|
||||
import { initSelectionListener } from './selection.js';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
console.log('[SaladDict] Content script loaded');
|
||||
logger.info('Content script loaded');
|
||||
|
||||
// 初始化文本选择监听
|
||||
initSelectionListener();
|
||||
|
||||
// Ping 测试 - 验证通信链路
|
||||
async function testCommunication() {
|
||||
console.log('[SaladDict] Testing communication...');
|
||||
logger.info('Testing communication...');
|
||||
|
||||
// 测试 1: 简单 Ping
|
||||
const pingResult = await ping();
|
||||
console.log('[SaladDict] Ping test:', pingResult ? '✅ OK' : '❌ Failed');
|
||||
logger.info('Ping test:', pingResult ? 'OK' : 'Failed');
|
||||
|
||||
// 测试 2: Echo 消息
|
||||
try {
|
||||
const echoResult = await messaging.sendToBackground('ECHO', { message: 'Hello from content' });
|
||||
console.log('[SaladDict] Echo test:', echoResult);
|
||||
logger.info('Echo test:', echoResult);
|
||||
} catch (error) {
|
||||
console.error('[SaladDict] Echo test failed:', error.message);
|
||||
logger.error('Echo test failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 延迟执行测试,确保 background 已就绪
|
||||
// 延迟执行测试
|
||||
setTimeout(testCommunication, 1000);
|
||||
|
||||
// 监听来自 Background 的消息
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
console.log('[SaladDict] Content received message:', message);
|
||||
logger.info('Content received message:', message);
|
||||
sendResponse({ received: true });
|
||||
return true;
|
||||
});
|
||||
|
||||
20
src/content/logger.js
Normal file
20
src/content/logger.js
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @file Content Script 日志工具
|
||||
*/
|
||||
|
||||
const NAMESPACE = '[SaladDict]';
|
||||
|
||||
export const logger = {
|
||||
debug(...args) {
|
||||
console.debug(NAMESPACE, ...args);
|
||||
},
|
||||
info(...args) {
|
||||
console.info(NAMESPACE, ...args);
|
||||
},
|
||||
warn(...args) {
|
||||
console.warn(NAMESPACE, ...args);
|
||||
},
|
||||
error(...args) {
|
||||
console.error(NAMESPACE, ...args);
|
||||
}
|
||||
};
|
||||
169
src/content/selection.js
Normal file
169
src/content/selection.js
Normal file
@ -0,0 +1,169 @@
|
||||
/**
|
||||
* @file 文本选择检测
|
||||
* @description 监听网页文本选择事件
|
||||
*/
|
||||
|
||||
import { logger } from './logger.js';
|
||||
import { createSaladIcon } from './components/SaladIcon.js';
|
||||
|
||||
let currentIcon = null;
|
||||
|
||||
/**
|
||||
* 获取当前选中的文本
|
||||
* @returns {string} 选中的文本
|
||||
*/
|
||||
export function getSelectedText() {
|
||||
const selection = window.getSelection();
|
||||
return selection ? selection.toString().trim() : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选中文本的坐标信息
|
||||
* @returns {Object|null} {x, y, width, height, left, top, right, bottom} 或 null
|
||||
*/
|
||||
export function getSelectionCoords() {
|
||||
const selection = window.getSelection();
|
||||
|
||||
if (!selection || selection.rangeCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
const rects = range.getClientRects();
|
||||
|
||||
if (rects.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取第一个矩形(单行)或整个选区的矩形(多行取首行)
|
||||
const firstRect = rects[0];
|
||||
|
||||
// 计算整个选区的包围矩形
|
||||
let minX = firstRect.left;
|
||||
let minY = firstRect.top;
|
||||
let maxX = firstRect.right;
|
||||
let maxY = firstRect.bottom;
|
||||
|
||||
for (const rect of rects) {
|
||||
minX = Math.min(minX, rect.left);
|
||||
minY = Math.min(minY, rect.top);
|
||||
maxX = Math.max(maxX, rect.right);
|
||||
maxY = Math.max(maxY, rect.bottom);
|
||||
}
|
||||
|
||||
return {
|
||||
// 首行位置(用于图标定位)
|
||||
x: firstRect.left,
|
||||
y: firstRect.top,
|
||||
width: firstRect.width,
|
||||
height: firstRect.height,
|
||||
left: firstRect.left,
|
||||
top: firstRect.top,
|
||||
right: firstRect.right,
|
||||
bottom: firstRect.bottom,
|
||||
// 整个选区的包围矩形
|
||||
boundingX: minX,
|
||||
boundingY: minY,
|
||||
boundingWidth: maxX - minX,
|
||||
boundingHeight: maxY - minY
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有有效文本被选中
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function hasSelection() {
|
||||
const text = getSelectedText();
|
||||
return text.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标抬起事件
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
function handleMouseUp(event) {
|
||||
// 延迟执行,等待选区完成
|
||||
setTimeout(() => {
|
||||
const selectedText = getSelectedText();
|
||||
|
||||
if (selectedText.length > 0) {
|
||||
const coords = getSelectionCoords();
|
||||
|
||||
logger.info('Text selected:', selectedText);
|
||||
console.log('[SaladDict] Selected text:', selectedText);
|
||||
|
||||
if (coords) {
|
||||
console.log('[SaladDict] Selection coords:', {
|
||||
x: coords.x,
|
||||
y: coords.y,
|
||||
width: coords.width,
|
||||
height: coords.height
|
||||
});
|
||||
|
||||
// 显示沙拉图标在选中文本右上角
|
||||
showSaladIcon(coords.x + coords.width, coords.y);
|
||||
}
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示沙拉图标
|
||||
* @param {number} x - X坐标
|
||||
* @param {number} y - Y坐标
|
||||
*/
|
||||
function showSaladIcon(x, y) {
|
||||
// 隐藏旧图标
|
||||
if (currentIcon) {
|
||||
currentIcon.destroy();
|
||||
currentIcon = null;
|
||||
}
|
||||
|
||||
// 创建新图标,传入点击回调
|
||||
currentIcon = createSaladIcon(x, y, {
|
||||
onClick: (event) => {
|
||||
logger.info('Icon click callback triggered');
|
||||
// 点击后图标不消失(后续再处理)
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[SaladDict] Icon shown at:', x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理页面点击事件(用于隐藏图标)
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
function handleDocumentClick(event) {
|
||||
// 如果点击的不是图标元素,则隐藏图标
|
||||
if (currentIcon && currentIcon.element !== event.target && !currentIcon.element.contains(event.target)) {
|
||||
currentIcon.destroy();
|
||||
currentIcon = null;
|
||||
logger.info('Icon hidden by document click');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化文本选择监听
|
||||
*/
|
||||
export function initSelectionListener() {
|
||||
logger.info('Initializing selection listener');
|
||||
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
// 延迟添加点击监听,避免与 mouseup 冲突
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', handleDocumentClick);
|
||||
}, 100);
|
||||
|
||||
logger.info('Selection listener initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁文本选择监听
|
||||
*/
|
||||
export function destroySelectionListener() {
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.removeEventListener('click', handleDocumentClick);
|
||||
logger.info('Selection listener destroyed');
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user