From 971a0c9dd9b3e7dea04f9078f971848c19f5eb8a 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 14:22:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(M3.9):=20=E6=9C=89=E9=81=93=E8=AF=8D?= =?UTF-8?q?=E5=85=B8=E5=AE=9E=E7=8E=B0=20(v0.2.9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/QUICK_REF.md | 2 +- docs/README.md | 6 +- docs/VERSION.md | 2 +- manifest.json | 2 +- package.json | 2 +- src/background/index.js | 3 +- src/content/components/DictPanel.js | 76 +++++++++- src/content/selection.js | 6 +- src/shared/dictionary/index.js | 1 + src/shared/dictionary/youdao.js | 219 ++++++++++++++++++++++++++++ 10 files changed, 304 insertions(+), 15 deletions(-) create mode 100644 src/shared/dictionary/youdao.js diff --git a/docs/QUICK_REF.md b/docs/QUICK_REF.md index 3489f01..758fb58 100644 --- a/docs/QUICK_REF.md +++ b/docs/QUICK_REF.md @@ -7,7 +7,7 @@ ## 版本速查 ### 当前版本 -`0.2.8` → 下一目标 `0.2.9` ([M3.9](./M3.md)) +`0.2.9` → 下一目标 `0.2.10` ([M3.10](./M3.md)) ### 模块版本范围 diff --git a/docs/README.md b/docs/README.md index df763ca..49abc4b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -65,9 +65,9 @@ M11.10完成 → 1.0.0 (正式发布) ## 当前状态 -**当前版本**: `0.2.8` -**当前进度**: 22/97 (23%) -**下一任务**: [M3.9 有道词典实现](./M3.md#m39-有道词典实现--目标版本-029) +**当前版本**: `0.2.9` +**当前进度**: 23/97 (24%) +**下一任务**: [M3.10 结果折叠/展开](./M3.md#m310-结果折叠展开--目标版本-0210) --- diff --git a/docs/VERSION.md b/docs/VERSION.md index 5e2c094..e2b2baa 100644 --- a/docs/VERSION.md +++ b/docs/VERSION.md @@ -44,7 +44,7 @@ | M3.6 | 0.2.6 | 点击图标查词(Mock) | ✅ | 2026-02-11 | | M3.7 | 0.2.7 | 必应词典真实API | ✅ | 2026-02-11 | | M3.8 | 0.2.8 | 加载状态显示 | ✅ | 2026-02-11 | -| M3.9 | 0.2.9 | 有道词典实现 | ⬜ | - | +| M3.9 | 0.2.9 | 有道词典实现 | ✅ | 2026-02-11 | | M3.10 | 0.2.10 | 结果折叠/展开 | ⬜ | - | | M3.11 | 0.2.11 | 词典图标标识 | ⬜ | - | diff --git a/manifest.json b/manifest.json index cb0463e..e46eca0 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "沙拉查词", - "version": "0.2.8", + "version": "0.2.9", "description": "聚合词典划词翻译", "permissions": [ "storage", diff --git a/package.json b/package.json index 1aed40e..b254f97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salad-dict", - "version": "0.2.8", + "version": "0.2.9", "description": "聚合词典划词翻译", "private": true, "type": "module", diff --git a/src/background/index.js b/src/background/index.js index c85f94d..6aca3d7 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -1,6 +1,6 @@ // Background Service Worker import { backgroundHandler } from '../shared/messaging.js'; -import { dictionaryManager, bingDictionary } from '../shared/dictionary/index.js'; +import { dictionaryManager, bingDictionary, youdaoDictionary } from '../shared/dictionary/index.js'; console.log('[SaladDict] Background service worker started'); @@ -14,6 +14,7 @@ console.log('[SaladDict] Message handler initialized'); // 注册词典到管理器 dictionaryManager.register('bing', bingDictionary); +dictionaryManager.register('youdao', youdaoDictionary); console.log('[SaladDict] Registered dictionaries:', dictionaryManager.getNames()); // 注册词典查询处理器 diff --git a/src/content/components/DictPanel.js b/src/content/components/DictPanel.js index 73e2aff..67df936 100644 --- a/src/content/components/DictPanel.js +++ b/src/content/components/DictPanel.js @@ -166,6 +166,24 @@ export class DictPanel { .loading-text { font-size: 14px; } + + .dict-section { + margin-bottom: 16px; + padding-bottom: 16px; + border-bottom: 1px solid #eee; + } + + .dict-section:last-child { + border-bottom: none; + } + + .dict-name { + font-size: 12px; + font-weight: bold; + color: #4CAF50; + margin-bottom: 8px; + text-transform: uppercase; + }
词典结果
@@ -322,7 +340,7 @@ export class DictPanel { } /** - * 渲染词典查询结果 + * 渲染单个词典查询结果 * @param {Object} result - 查询结果 * @param {string} result.word - 单词 * @param {string} [result.phonetic] - 音标 @@ -330,10 +348,8 @@ export class DictPanel { * @param {Array} [result.examples] - 例句列表 */ renderResult(result) { - const content = this.element.shadowRoot.querySelector('.content'); - if (!result) { - content.innerHTML = '
暂无查询结果
'; + this._setContent('
暂无查询结果
'); return; } @@ -345,6 +361,58 @@ export class DictPanel { ${this._renderExamples(result.examples)} `; + this._setContent(html); + } + + /** + * 渲染多个词典查询结果 + * @param {Array} results - 多个词典结果 {name, result}[] + */ + renderMultipleResults(results) { + if (!results || results.length === 0) { + this._setContent('
暂无查询结果
'); + return; + } + + // 第一个结果作为主结果显示单词标题 + const firstResult = results[0].result; + let html = ` +
${this._escapeHtml(firstResult.word)}
+ `; + + // 显示每个词典的结果 + for (const { name, result } of results) { + html += this._renderDictSection(name, result); + } + + this._setContent(html); + } + + /** + * 渲染单个词典区块 + * @private + * @param {string} dictName - 词典名称 + * @param {Object} result - 词典结果 + * @returns {string} HTML 字符串 + */ + _renderDictSection(dictName, result) { + return ` +
+
${this._escapeHtml(dictName)}
+ ${result.phonetic ? `
${this._escapeHtml(result.phonetic)}
` : ''} + ${this._renderMeanings(result.meanings)} + ${this._renderExamples(result.examples)} +
+ `; + } + + /** + * 设置内容区域 HTML + * @private + * @param {string} html - HTML 内容 + */ + _setContent(html) { + const content = this.element.shadowRoot.querySelector('.content'); content.innerHTML = html; } diff --git a/src/content/selection.js b/src/content/selection.js index 52d8961..7ccee3a 100644 --- a/src/content/selection.js +++ b/src/content/selection.js @@ -190,9 +190,9 @@ function showSaladIcon(x, y) { // 显示查询结果 if (response?.results && response.results.length > 0) { - const firstResult = response.results[0].result; - currentPanel.renderResult(firstResult); - logger.info('Search result rendered:', firstResult.word); + // 显示所有词典结果 + currentPanel.renderMultipleResults(response.results); + logger.info('Search results rendered:', response.results.length, 'dictionaries'); } else { currentPanel.renderResult({ word: selectedText, diff --git a/src/shared/dictionary/index.js b/src/shared/dictionary/index.js index 70adae6..4ed6fc5 100644 --- a/src/shared/dictionary/index.js +++ b/src/shared/dictionary/index.js @@ -6,3 +6,4 @@ export { DictionaryBase, createResult, createMeaning, createExample } from './base.js'; export { DictionaryManager, dictionaryManager } from './manager.js'; export { BingDictionary, bingDictionary } from './bing.js'; +export { YoudaoDictionary, youdaoDictionary } from './youdao.js'; diff --git a/src/shared/dictionary/youdao.js b/src/shared/dictionary/youdao.js new file mode 100644 index 0000000..00c77da --- /dev/null +++ b/src/shared/dictionary/youdao.js @@ -0,0 +1,219 @@ +/** + * @file 有道词典实现 + * @description 有道词典的实现,通过 HTTP 请求获取数据 + */ + +import { DictionaryBase, createResult, createMeaning, createExample } from './base.js'; +import { messaging } from '../messaging.js'; + +/** + * 有道词典实现 + */ +export class YoudaoDictionary extends DictionaryBase { + constructor(config = {}) { + super({ + name: '有道词典', + icon: 'icons/youdao.png', + languages: ['en', 'zh'], + ...config + }); + } + + /** + * 查询单词 + * @param {string} word - 要查询的单词 + * @returns {Promise} 查询结果 + */ + async search(word) { + if (!word?.trim()) { + throw new Error('查询单词不能为空'); + } + + const trimmedWord = word.trim(); + const url = `https://dict.youdao.com/result?word=${encodeURIComponent(trimmedWord)}&lang=en`; + + try { + // 通过 Background 发起 HTTP 请求 + const response = await messaging.sendToBackground('HTTP.GET', { url }, 10000); + + if (!response?.text) { + throw new Error('获取词典数据失败'); + } + + // 解析 HTML 提取数据 + return this._parseHtml(response.text, trimmedWord, url); + } catch (error) { + console.error('[YoudaoDictionary] Search failed:', error); + + // 返回友好错误提示 + return createResult({ + word: trimmedWord, + phonetic: '', + meanings: [createMeaning('提示', ['查询失败,请检查网络连接'])], + examples: [], + url + }); + } + } + + /** + * 解析有道词典 HTML + * @private + * @param {string} html - HTML 内容 + * @param {string} word - 查询的单词 + * @param {string} url - 查询 URL + * @returns {DictionaryResult} 解析结果 + */ + _parseHtml(html, word, url) { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + + // 提取音标 + const phonetic = this._extractPhonetic(doc); + + // 提取释义 + const meanings = this._extractMeanings(doc); + + // 提取例句 + const examples = this._extractExamples(doc); + + return createResult({ + word, + phonetic, + meanings, + examples, + url + }); + } + + /** + * 提取音标 + * @private + * @param {Document} doc - HTML 文档 + * @returns {string} 音标 + */ + _extractPhonetic(doc) { + // 尝试多个可能的选择器 + const selectors = [ + '.phonetic', // 主要音标类 + '.pronounce', // 发音区域 + '[class*="phonetic"]', // 包含 phonetic 的类 + '.word-info .phonetic' // 单词信息区的音标 + ]; + + for (const selector of selectors) { + const elements = doc.querySelectorAll(selector); + for (const el of elements) { + const text = el.textContent?.trim(); + if (text && (text.includes('/') || text.includes('['))) { + return text; + } + } + } + + // 正则提取 fallback + const bodyText = doc.body?.textContent || ''; + const match = bodyText.match(/\[[\u0250-\u02AEˈˌ]+\]/); + if (match) { + return match[0]; + } + + return ''; + } + + /** + * 提取释义 + * @private + * @param {Document} doc - HTML 文档 + * @returns {Array} 释义列表 + */ + _extractMeanings(doc) { + const meanings = []; + + // 尝试多个可能的选择器 + const selectors = [ + '.trans-container ul li', // 主要释义列表 + '.basic .word-exp', // 基本释义 + '.meaning li', // 备选释义 + '[class*="meaning"] li', // 包含 meaning 的类 + '.content ul li' // 通用内容列表 + ]; + + for (const selector of selectors) { + const items = doc.querySelectorAll(selector); + + for (const item of items) { + const text = item.textContent?.trim(); + if (!text) continue; + + // 尝试匹配词性和释义 + const match = text.match(/^([a-zA-Z]+\.?)\s*(.+)$/); + if (match) { + const partOfSpeech = match[1]; + const defsText = match[2]; + + // 分割多个释义 + const definitions = defsText + .split(/[;;]/) + .map(d => d.trim()) + .filter(d => d.length > 0); + + if (definitions.length > 0) { + meanings.push(createMeaning(partOfSpeech, definitions)); + } + } else if (text.length > 0 && text.length < 50) { + // 没有词性标记的释义 + meanings.push(createMeaning('', [text])); + } + } + + if (meanings.length > 0) break; + } + + return meanings.length > 0 ? meanings : [createMeaning('n.', ['暂无释义'])]; + } + + /** + * 提取例句 + * @private + * @param {Document} doc - HTML 文档 + * @returns {Array} 例句列表 + */ + _extractExamples(doc) { + const examples = []; + + // 尝试多个可能的选择器 + const selectors = [ + '.examples li', // 主要例句列表 + '.example-item', // 例句项 + '.sentence', // 句子区域 + '[class*="example"] li', // 包含 example 的类 + '.content .ex_li' // 通用例句 + ]; + + for (const selector of selectors) { + const items = doc.querySelectorAll(selector); + + for (const item of items) { + const enEl = item.querySelector('.en-sentence, .english, [class*="english"]'); + const cnEl = item.querySelector('.cn-sentence, .chinese, [class*="chinese"]'); + + const sentence = enEl?.textContent?.trim() || item.textContent?.trim(); + const translation = cnEl?.textContent?.trim() || ''; + + if (sentence) { + examples.push(createExample(sentence, translation)); + } + + if (examples.length >= 2) break; + } + + if (examples.length > 0) break; + } + + return examples; + } +} + +// 导出单例实例 +export const youdaoDictionary = new YoudaoDictionary();