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();