feat(M3.9): 有道词典实现 (v0.2.9)
This commit is contained in:
parent
ff6c884744
commit
971a0c9dd9
@ -7,7 +7,7 @@
|
|||||||
## 版本速查
|
## 版本速查
|
||||||
|
|
||||||
### 当前版本
|
### 当前版本
|
||||||
`0.2.8` → 下一目标 `0.2.9` ([M3.9](./M3.md))
|
`0.2.9` → 下一目标 `0.2.10` ([M3.10](./M3.md))
|
||||||
|
|
||||||
### 模块版本范围
|
### 模块版本范围
|
||||||
|
|
||||||
|
|||||||
@ -65,9 +65,9 @@ M11.10完成 → 1.0.0 (正式发布)
|
|||||||
|
|
||||||
## 当前状态
|
## 当前状态
|
||||||
|
|
||||||
**当前版本**: `0.2.8`
|
**当前版本**: `0.2.9`
|
||||||
**当前进度**: 22/97 (23%)
|
**当前进度**: 23/97 (24%)
|
||||||
**下一任务**: [M3.9 有道词典实现](./M3.md#m39-有道词典实现--目标版本-029)
|
**下一任务**: [M3.10 结果折叠/展开](./M3.md#m310-结果折叠展开--目标版本-0210)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,7 @@
|
|||||||
| M3.6 | 0.2.6 | 点击图标查词(Mock) | ✅ | 2026-02-11 |
|
| M3.6 | 0.2.6 | 点击图标查词(Mock) | ✅ | 2026-02-11 |
|
||||||
| M3.7 | 0.2.7 | 必应词典真实API | ✅ | 2026-02-11 |
|
| M3.7 | 0.2.7 | 必应词典真实API | ✅ | 2026-02-11 |
|
||||||
| M3.8 | 0.2.8 | 加载状态显示 | ✅ | 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.10 | 0.2.10 | 结果折叠/展开 | ⬜ | - |
|
||||||
| M3.11 | 0.2.11 | 词典图标标识 | ⬜ | - |
|
| M3.11 | 0.2.11 | 词典图标标识 | ⬜ | - |
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "沙拉查词",
|
"name": "沙拉查词",
|
||||||
"version": "0.2.8",
|
"version": "0.2.9",
|
||||||
"description": "聚合词典划词翻译",
|
"description": "聚合词典划词翻译",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "salad-dict",
|
"name": "salad-dict",
|
||||||
"version": "0.2.8",
|
"version": "0.2.9",
|
||||||
"description": "聚合词典划词翻译",
|
"description": "聚合词典划词翻译",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Background Service Worker
|
// Background Service Worker
|
||||||
import { backgroundHandler } from '../shared/messaging.js';
|
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');
|
console.log('[SaladDict] Background service worker started');
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ console.log('[SaladDict] Message handler initialized');
|
|||||||
|
|
||||||
// 注册词典到管理器
|
// 注册词典到管理器
|
||||||
dictionaryManager.register('bing', bingDictionary);
|
dictionaryManager.register('bing', bingDictionary);
|
||||||
|
dictionaryManager.register('youdao', youdaoDictionary);
|
||||||
console.log('[SaladDict] Registered dictionaries:', dictionaryManager.getNames());
|
console.log('[SaladDict] Registered dictionaries:', dictionaryManager.getNames());
|
||||||
|
|
||||||
// 注册词典查询处理器
|
// 注册词典查询处理器
|
||||||
|
|||||||
@ -166,6 +166,24 @@ export class DictPanel {
|
|||||||
.loading-text {
|
.loading-text {
|
||||||
font-size: 14px;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="header">词典结果</div>
|
<div class="header">词典结果</div>
|
||||||
@ -322,7 +340,7 @@ export class DictPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染词典查询结果
|
* 渲染单个词典查询结果
|
||||||
* @param {Object} result - 查询结果
|
* @param {Object} result - 查询结果
|
||||||
* @param {string} result.word - 单词
|
* @param {string} result.word - 单词
|
||||||
* @param {string} [result.phonetic] - 音标
|
* @param {string} [result.phonetic] - 音标
|
||||||
@ -330,10 +348,8 @@ export class DictPanel {
|
|||||||
* @param {Array} [result.examples] - 例句列表
|
* @param {Array} [result.examples] - 例句列表
|
||||||
*/
|
*/
|
||||||
renderResult(result) {
|
renderResult(result) {
|
||||||
const content = this.element.shadowRoot.querySelector('.content');
|
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
content.innerHTML = '<div class="placeholder">暂无查询结果</div>';
|
this._setContent('<div class="placeholder">暂无查询结果</div>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,6 +361,58 @@ export class DictPanel {
|
|||||||
${this._renderExamples(result.examples)}
|
${this._renderExamples(result.examples)}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
this._setContent(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染多个词典查询结果
|
||||||
|
* @param {Array} results - 多个词典结果 {name, result}[]
|
||||||
|
*/
|
||||||
|
renderMultipleResults(results) {
|
||||||
|
if (!results || results.length === 0) {
|
||||||
|
this._setContent('<div class="placeholder">暂无查询结果</div>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第一个结果作为主结果显示单词标题
|
||||||
|
const firstResult = results[0].result;
|
||||||
|
let html = `
|
||||||
|
<div class="word-title">${this._escapeHtml(firstResult.word)}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 显示每个词典的结果
|
||||||
|
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 `
|
||||||
|
<div class="dict-section">
|
||||||
|
<div class="dict-name">${this._escapeHtml(dictName)}</div>
|
||||||
|
${result.phonetic ? `<div class="phonetic" style="margin-bottom: 8px;">${this._escapeHtml(result.phonetic)}</div>` : ''}
|
||||||
|
${this._renderMeanings(result.meanings)}
|
||||||
|
${this._renderExamples(result.examples)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置内容区域 HTML
|
||||||
|
* @private
|
||||||
|
* @param {string} html - HTML 内容
|
||||||
|
*/
|
||||||
|
_setContent(html) {
|
||||||
|
const content = this.element.shadowRoot.querySelector('.content');
|
||||||
content.innerHTML = html;
|
content.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -190,9 +190,9 @@ function showSaladIcon(x, y) {
|
|||||||
|
|
||||||
// 显示查询结果
|
// 显示查询结果
|
||||||
if (response?.results && response.results.length > 0) {
|
if (response?.results && response.results.length > 0) {
|
||||||
const firstResult = response.results[0].result;
|
// 显示所有词典结果
|
||||||
currentPanel.renderResult(firstResult);
|
currentPanel.renderMultipleResults(response.results);
|
||||||
logger.info('Search result rendered:', firstResult.word);
|
logger.info('Search results rendered:', response.results.length, 'dictionaries');
|
||||||
} else {
|
} else {
|
||||||
currentPanel.renderResult({
|
currentPanel.renderResult({
|
||||||
word: selectedText,
|
word: selectedText,
|
||||||
|
|||||||
@ -6,3 +6,4 @@
|
|||||||
export { DictionaryBase, createResult, createMeaning, createExample } from './base.js';
|
export { DictionaryBase, createResult, createMeaning, createExample } from './base.js';
|
||||||
export { DictionaryManager, dictionaryManager } from './manager.js';
|
export { DictionaryManager, dictionaryManager } from './manager.js';
|
||||||
export { BingDictionary, bingDictionary } from './bing.js';
|
export { BingDictionary, bingDictionary } from './bing.js';
|
||||||
|
export { YoudaoDictionary, youdaoDictionary } from './youdao.js';
|
||||||
|
|||||||
219
src/shared/dictionary/youdao.js
Normal file
219
src/shared/dictionary/youdao.js
Normal file
@ -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<DictionaryResult>} 查询结果
|
||||||
|
*/
|
||||||
|
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<Meaning>} 释义列表
|
||||||
|
*/
|
||||||
|
_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<Example>} 例句列表
|
||||||
|
*/
|
||||||
|
_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();
|
||||||
Loading…
x
Reference in New Issue
Block a user