diff --git a/src/content/components/DictPanel.js b/src/content/components/DictPanel.js
index ce16260..4997497 100644
--- a/src/content/components/DictPanel.js
+++ b/src/content/components/DictPanel.js
@@ -55,7 +55,7 @@ export class DictPanel {
.header {
padding: 12px 16px;
- background-color: #4CAF50;
+ background-color: #5DBE8C;
color: white;
font-size: 16px;
font-weight: 500;
@@ -65,7 +65,7 @@ export class DictPanel {
}
.header:hover {
- background-color: #45a049;
+ background-color: #4da87a;
}
.content {
@@ -75,6 +75,7 @@ export class DictPanel {
font-size: 14px;
line-height: 1.6;
color: #333;
+ background: #f5f5f5;
}
.placeholder {
@@ -83,11 +84,16 @@ export class DictPanel {
margin-top: 100px;
}
+ /* 单词标题 */
.word-title {
- font-size: 28px;
+ font-size: 32px;
font-weight: bold;
color: #333;
margin-bottom: 4px;
+ background: #fff;
+ padding: 12px 16px;
+ border-radius: 8px;
+ margin: -16px -16px 16px -16px;
}
.phonetic {
@@ -97,125 +103,46 @@ export class DictPanel {
font-family: 'Times New Roman', serif;
}
- .meanings-section {
- margin-bottom: 16px;
- }
-
- .section-title {
- font-size: 12px;
- font-weight: bold;
- color: #4CAF50;
- margin-bottom: 8px;
- text-transform: uppercase;
- }
-
- .meaning-item {
- margin-bottom: 8px;
- }
-
- .part-of-speech {
- color: #2196F3;
- font-weight: 500;
- margin-right: 8px;
- }
-
- .definition {
- color: #333;
- }
-
- .examples-section {
- margin-top: 16px;
- }
-
- .example-item {
+ /* 词典区块 */
+ .dict-section {
margin-bottom: 12px;
- padding: 8px;
- background: #f5f5f5;
- border-radius: 4px;
- }
-
- .example-sentence {
- color: #333;
- margin-bottom: 4px;
- }
-
- .example-translation {
- color: #666;
- font-size: 13px;
- }
-
- .loading {
- text-align: center;
- padding: 40px 20px;
- color: #666;
- }
-
- .loading-spinner {
- width: 32px;
- height: 32px;
- border: 3px solid #e0e0e0;
- border-top-color: #4CAF50;
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- margin: 0 auto 16px;
- }
-
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
-
- .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-section {
- margin-bottom: 8px;
- border: 1px solid #e0e0e0;
- border-radius: 6px;
+ background: #fff;
+ border-radius: 8px;
overflow: hidden;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.dict-header {
display: flex;
align-items: center;
padding: 10px 12px;
- background: #f8f8f8;
+ background: #fff;
cursor: pointer;
user-select: none;
- transition: background 0.2s;
+ border-bottom: 1px solid #f0f0f0;
}
.dict-header:hover {
- background: #f0f0f0;
+ background: #fafafa;
}
.dict-toggle {
font-size: 10px;
- margin-right: 6px;
- color: #666;
+ margin-right: 8px;
+ color: #999;
width: 12px;
text-align: center;
}
.dict-icon {
- width: 16px;
- height: 16px;
+ width: 18px;
+ height: 18px;
border-radius: 3px;
margin-right: 8px;
display: flex;
align-items: center;
justify-content: center;
- font-size: 10px;
+ font-size: 11px;
font-weight: bold;
color: white;
flex-shrink: 0;
@@ -234,19 +161,103 @@ export class DictPanel {
}
.dict-name {
- font-size: 13px;
+ font-size: 14px;
font-weight: 600;
color: #333;
+ flex: 1;
}
.dict-content {
- padding: 12px;
+ padding: 12px 16px;
display: block;
}
.dict-content.collapsed {
display: none;
}
+
+ /* 释义 */
+ .meanings-section {
+ margin-bottom: 12px;
+ }
+
+ .section-title {
+ font-size: 12px;
+ font-weight: 600;
+ color: #4CAF50;
+ margin-bottom: 8px;
+ }
+
+ .meaning-item {
+ margin-bottom: 6px;
+ font-size: 14px;
+ }
+
+ .part-of-speech {
+ color: #2196F3;
+ font-weight: 500;
+ margin-right: 8px;
+ }
+
+ .definition {
+ color: #333;
+ }
+
+ /* 例句 */
+ .examples-section {
+ margin-top: 12px;
+ }
+
+ .example-item {
+ margin-bottom: 10px;
+ padding: 10px 12px;
+ background: #f8f8f8;
+ border-radius: 6px;
+ }
+
+ .example-number {
+ color: #999;
+ margin-right: 6px;
+ }
+
+ .example-sentence {
+ color: #333;
+ margin-bottom: 4px;
+ line-height: 1.5;
+ }
+
+ .example-translation {
+ color: #666;
+ font-size: 13px;
+ line-height: 1.5;
+ }
+
+ /* 加载状态 */
+ .loading {
+ text-align: center;
+ padding: 40px 20px;
+ color: #666;
+ background: #fff;
+ border-radius: 8px;
+ }
+
+ .loading-spinner {
+ width: 32px;
+ height: 32px;
+ border: 3px solid #e0e0e0;
+ border-top-color: #5DBE8C;
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+ margin: 0 auto 16px;
+ }
+
+ @keyframes spin {
+ to { transform: rotate(360deg); }
+ }
+
+ .loading-text {
+ font-size: 14px;
+ }
@@ -614,9 +625,12 @@ export class DictPanel {
// 最多显示2个例句
const displayExamples = examples.slice(0, 2);
- const examplesHtml = displayExamples.map(e => `
+ const examplesHtml = displayExamples.map((e, index) => `
-
${this._escapeHtml(e.sentence || '')}
+
+ ${index + 1}.
+ ${this._escapeHtml(e.sentence || '')}
+
${e.translation ? `
${this._escapeHtml(e.translation)}
` : ''}
`).join('');
diff --git a/src/shared/dictionary/bing.js b/src/shared/dictionary/bing.js
index 86ccf2c..ccf1a8c 100644
--- a/src/shared/dictionary/bing.js
+++ b/src/shared/dictionary/bing.js
@@ -46,7 +46,6 @@ export class BingDictionary extends DictionaryBase {
const html = await response.text();
- // 使用正则提取数据
return this._parseHtml(html, trimmedWord, url);
} catch (error) {
console.error('[BingDictionary] Search failed:', error);
@@ -62,7 +61,17 @@ export class BingDictionary extends DictionaryBase {
}
/**
- * 解析必应词典 HTML(使用正则)
+ * 去除 HTML 标签
+ * @private
+ * @param {string} html - 包含 HTML 的字符串
+ * @returns {string} 纯文本
+ */
+ _stripHtml(html) {
+ return html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
+ }
+
+ /**
+ * 解析必应词典 HTML
* @private
* @param {string} html - HTML 内容
* @param {string} word - 查询的单词
@@ -86,16 +95,24 @@ export class BingDictionary extends DictionaryBase {
* @returns {string} 音标
*/
_extractPhonetic(html) {
- // 匹配音标格式如 [həˈləʊ] 或 /həˈləʊ/
- const match = html.match(/\[[\u0250-\u02AEˈˌa-zA-Z]+\]/);
- if (match) {
- return match[0];
+ // 必应词典音标通常在 hd_pr 或 hd_tf 类中
+ const patterns = [
+ /
]*class="[^"]*hd_pr[^"]*"[^>]*>(\[[^\]]+\])<\/span>/i,
+ /]*class="[^"]*hd_tf[^"]*"[^>]*>(\[[^\]]+\])<\/span>/i,
+ /]*class="[^"]*pron[^"]*"[^>]*>(\[[^\]]+\])<\/span>/i
+ ];
+
+ for (const pattern of patterns) {
+ const match = html.match(pattern);
+ if (match) {
+ return match[1].trim();
+ }
}
-
- // 备选:匹配 /.../ 格式
- const match2 = html.match(/\/[\u0250-\u02AEˈˌa-zA-Z]+\//);
- if (match2) {
- return match2[0];
+
+ // 通用音标匹配
+ const genericMatch = html.match(/(\[[\u0250-\u02AEˈˌa-zA-Z\s]+\])/);
+ if (genericMatch) {
+ return genericMatch[1];
}
return '';
@@ -109,38 +126,47 @@ export class BingDictionary extends DictionaryBase {
*/
_extractMeanings(html) {
const meanings = [];
+ const seen = new Set();
- // 尝试匹配常见的词典释义格式
- // 格式1: n.定义
- const posDefPattern = /<[^>]*class="[^"]*(?:pos|web)[^"]*"[^>]*>([^<]+)<\/[^>]*>\s*<[^>]*class="[^"]*(?:def|tran)[^"]*"[^>]*>([^<]+)/gi;
+ // 必应词典释义结构:
+ // n. 释义
+ // 或者:
+ // 词性. 释义
+
+ // 模式1: 标准词典释义格式
+ const defPattern = /]*>\s*]*class="[^"]*pos[^"]*"[^>]*>([^<]+)<\/span>\s*]*class="[^"]*def[^"]*"[^>]*>([^<]+)<\/span>/gi;
let match;
- while ((match = posDefPattern.exec(html)) !== null) {
- const partOfSpeech = match[1].trim();
- const definition = match[2].trim();
- if (partOfSpeech && definition) {
- meanings.push(createMeaning(partOfSpeech, [definition]));
+ while ((match = defPattern.exec(html)) !== null) {
+ const pos = this._stripHtml(match[1]).trim();
+ const def = this._stripHtml(match[2]).trim();
+
+ if (pos && def && !seen.has(`${pos}-${def}`)) {
+ seen.add(`${pos}-${def}`);
+ meanings.push(createMeaning(pos, [def]));
}
}
- // 格式2: 直接匹配 "词性. 释义" 格式
+ // 模式2: 备选释义格式
if (meanings.length === 0) {
- const simplePattern = /([a-z]+\.?)\s*([^<\n]{2,30})/gi;
- const seen = new Set();
+ const altPattern = /]*class="[^"]*(?:qdef|def|meaning)[^"]*"[^>]*>(.*?)<\/div>/gi;
- while ((match = simplePattern.exec(html)) !== null) {
- const partOfSpeech = match[1].trim();
- const definition = match[2].trim();
+ while ((match = altPattern.exec(html)) !== null) {
+ const content = match[1];
- // 过滤无效结果
- if (!partOfSpeech.match(/^[a-z]+\.?$/i)) continue;
- if (definition.length < 2 || definition.length > 30) continue;
- if (seen.has(definition)) continue;
+ // 提取词性和释义
+ const posMatches = content.matchAll(/
]*class="[^"]*(?:pos|web)[^"]*"[^>]*>([^<]+)<\/span>/gi);
+ const defMatches = content.matchAll(/]*class="[^"]*def[^"]*"[^>]*>([^<]+)<\/span>/gi);
- seen.add(definition);
- meanings.push(createMeaning(partOfSpeech, [definition]));
+ const poses = [...posMatches].map(m => this._stripHtml(m[1]).trim()).filter(Boolean);
+ const defs = [...defMatches].map(m => this._stripHtml(m[1]).trim()).filter(Boolean);
- if (meanings.length >= 5) break;
+ for (let i = 0; i < Math.min(poses.length, defs.length); i++) {
+ if (!seen.has(`${poses[i]}-${defs[i]}`)) {
+ seen.add(`${poses[i]}-${defs[i]}`);
+ meanings.push(createMeaning(poses[i], [defs[i]]));
+ }
+ }
}
}
@@ -155,24 +181,35 @@ export class BingDictionary extends DictionaryBase {
*/
_extractExamples(html) {
const examples = [];
-
- // 匹配例句模式:英文句子后跟中文翻译
- // 尝试匹配 或 中的例句
- const sentencePattern = /<[^>]*>([^<]{10,100}[a-zA-Z][^<]{0,50})<\/[^>]*>\s*<[^>]*>([^<]{5,50}[\u4e00-\u9fa5][^<]{0,50})<\/[^>]*>/gi;
-
- let match;
const seen = new Set();
- while ((match = sentencePattern.exec(html)) !== null) {
- const sentence = match[1].trim();
- const translation = match[2].trim();
+ // 必应例句格式:
+ //
+
+ // 提取所有例句块
+ const senPattern = /
]*class="[^"]*sen_li[^"]*"[^>]*>(.*?)<\/div>/gi;
+ const senMatches = [...html.matchAll(senPattern)];
+
+ for (const senMatch of senMatches) {
+ const senBlock = senMatch[1];
- if (seen.has(sentence)) continue;
- seen.add(sentence);
+ // 提取英文
+ const enMatch = senBlock.match(/
]*class="[^"]*sen_en[^"]*"[^>]*>(.*?)<\/div>/i);
+ // 提取中文
+ const cnMatch = senBlock.match(/
]*class="[^"]*sen_cn[^"]*"[^>]*>(.*?)<\/div>/i);
- // 验证:英文句子应该包含空格且长度合适
- if (sentence.length > 10 && sentence.length < 150 && sentence.includes(' ')) {
- examples.push(createExample(sentence, translation));
+ if (enMatch) {
+ const sentence = this._stripHtml(enMatch[1]).trim();
+ const translation = cnMatch ? this._stripHtml(cnMatch[1]).trim() : '';
+
+ // 过滤无效结果
+ if (sentence && sentence.length > 5 && !sentence.includes('<') && !seen.has(sentence)) {
+ seen.add(sentence);
+ examples.push(createExample(sentence, translation));
+ }
}
if (examples.length >= 2) break;
diff --git a/src/shared/dictionary/youdao.js b/src/shared/dictionary/youdao.js
index 9555258..6e04723 100644
--- a/src/shared/dictionary/youdao.js
+++ b/src/shared/dictionary/youdao.js
@@ -46,7 +46,6 @@ export class YoudaoDictionary extends DictionaryBase {
const html = await response.text();
- // 使用正则提取数据
return this._parseHtml(html, trimmedWord, url);
} catch (error) {
console.error('[YoudaoDictionary] Search failed:', error);
@@ -62,7 +61,17 @@ export class YoudaoDictionary extends DictionaryBase {
}
/**
- * 解析有道词典 HTML(使用正则)
+ * 去除 HTML 标签
+ * @private
+ * @param {string} html - 包含 HTML 的字符串
+ * @returns {string} 纯文本
+ */
+ _stripHtml(html) {
+ return html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
+ }
+
+ /**
+ * 解析有道词典 HTML
* @private
* @param {string} html - HTML 内容
* @param {string} word - 查询的单词
@@ -86,15 +95,24 @@ export class YoudaoDictionary extends DictionaryBase {
* @returns {string} 音标
*/
_extractPhonetic(html) {
- // 匹配音标格式
- const match = html.match(/\[[\u0250-\u02AEˈˌa-zA-Z]+\]/);
- if (match) {
- return match[0];
+ // 有道词典音标通常在 phonetic 或 pron 类中
+ const patterns = [
+ /
]*class="[^"]*phonetic[^"]*"[^>]*>(\[[^\]]+\])<\/span>/i,
+ /]*class="[^"]*pron[^"]*"[^>]*>(\[[^\]]+\])<\/span>/i,
+ /]*class="[^"]*phone[^"]*"[^>]*>([^<]+)<\/span>/i
+ ];
+
+ for (const pattern of patterns) {
+ const match = html.match(pattern);
+ if (match) {
+ return match[1].trim();
+ }
}
-
- const match2 = html.match(/\/[\u0250-\u02AEˈˌa-zA-Z]+\//);
- if (match2) {
- return match2[0];
+
+ // 通用音标匹配
+ const genericMatch = html.match(/(\[[\u0250-\u02AEˈˌa-zA-Z\s]+\])/);
+ if (genericMatch) {
+ return genericMatch[1];
}
return '';
@@ -108,36 +126,53 @@ export class YoudaoDictionary extends DictionaryBase {
*/
_extractMeanings(html) {
const meanings = [];
+ const seen = new Set();
- // 尝试匹配常见的词典释义格式
- const posDefPattern = /<[^>]*class="[^"]*(?:pos|trans)[^"]*"[^>]*>([^<]+)<\/[^>]*>\s*<[^>]*class="[^"]*(?:def|chn)[^"]*"[^>]*>([^<]+)/gi;
+ // 有道词典释义结构:
+ // n. 释义
+
+ // 模式1: 标准释义格式
+ const defPattern = /]*>\s*]*class="[^"]*pos[^"]*"[^>]*>([^<]+)<\/span>\s*]*class="[^"]*trans[^"]*"[^>]*>([^<]+)<\/span>/gi;
let match;
- while ((match = posDefPattern.exec(html)) !== null) {
- const partOfSpeech = match[1].trim();
- const definition = match[2].trim();
- if (partOfSpeech && definition) {
- meanings.push(createMeaning(partOfSpeech, [definition]));
+ while ((match = defPattern.exec(html)) !== null) {
+ const pos = this._stripHtml(match[1]).trim();
+ const def = this._stripHtml(match[2]).trim();
+
+ if (pos && def && !seen.has(`${pos}-${def}`)) {
+ seen.add(`${pos}-${def}`);
+ meanings.push(createMeaning(pos, [def]));
}
}
- // 备选:直接匹配 "词性. 释义" 格式
+ // 模式2: 备选释义格式 (trans-container 中的释义)
if (meanings.length === 0) {
- const simplePattern = /([a-z]+\.?)\s*([^<\n]{2,30})/gi;
- const seen = new Set();
+ const containerPattern = /]*class="[^"]*trans-container[^"]*"[^>]*>(.*?)<\/ul>/i;
+ const containerMatch = html.match(containerPattern);
- while ((match = simplePattern.exec(html)) !== null) {
- const partOfSpeech = match[1].trim();
- const definition = match[2].trim();
+ if (containerMatch) {
+ const container = containerMatch[1];
- if (!partOfSpeech.match(/^[a-z]+\.?$/i)) continue;
- if (definition.length < 2 || definition.length > 30) continue;
- if (seen.has(definition)) continue;
+ // 提取所有 li 项
+ const liPattern = /
]*>(.*?)<\/li>/gi;
+ const liMatches = [...container.matchAll(liPattern)];
- seen.add(definition);
- meanings.push(createMeaning(partOfSpeech, [definition]));
-
- if (meanings.length >= 5) break;
+ for (const liMatch of liMatches) {
+ const content = liMatch[1];
+
+ // 提取词性
+ const posMatch = content.match(/]*class="[^"]*pos[^"]*"[^>]*>([^<]+)<\/span>/i);
+ // 提取释义
+ const transMatch = content.match(/]*class="[^"]*(?:trans|chn)[^"]*"[^>]*>([^<]+)<\/span>/i);
+
+ const pos = posMatch ? this._stripHtml(posMatch[1]).trim() : 'n.';
+ const def = transMatch ? this._stripHtml(transMatch[1]).trim() : this._stripHtml(content);
+
+ if (def && !seen.has(`${pos}-${def}`)) {
+ seen.add(`${pos}-${def}`);
+ meanings.push(createMeaning(pos, [def]));
+ }
+ }
}
}
@@ -152,22 +187,33 @@ export class YoudaoDictionary extends DictionaryBase {
*/
_extractExamples(html) {
const examples = [];
-
- // 匹配例句模式
- const sentencePattern = /<[^>]*>([^<]{10,100}[a-zA-Z][^<]{0,50})<\/[^>]*>\s*<[^>]*>([^<]{5,50}[\u4e00-\u9fa5][^<]{0,50})<\/[^>]*>/gi;
-
- let match;
const seen = new Set();
- while ((match = sentencePattern.exec(html)) !== null) {
- const sentence = match[1].trim();
- const translation = match[2].trim();
+ // 有道例句格式:
+ //
+ // 英文例句
+ // 中文翻译
+ //
+
+ // 提取例句块
+ const senPattern = /]*class="[^"]*examples_li[^"]*"[^>]*>(.*?)<\/li>/gi;
+ const senMatches = [...html.matchAll(senPattern)];
+
+ for (const senMatch of senMatches) {
+ const senBlock = senMatch[1];
- if (seen.has(sentence)) continue;
- seen.add(sentence);
+ // 提取所有 p 标签内容
+ const pPattern = /]*class="[^"]*examples_p[^"]*"[^>]*>(.*?)<\/p>/gi;
+ const pMatches = [...senBlock.matchAll(pPattern)];
- if (sentence.length > 10 && sentence.length < 150 && sentence.includes(' ')) {
- examples.push(createExample(sentence, translation));
+ if (pMatches.length >= 2) {
+ const sentence = this._stripHtml(pMatches[0][1]).trim();
+ const translation = this._stripHtml(pMatches[1][1]).trim();
+
+ if (sentence && !sentence.includes('<') && !seen.has(sentence)) {
+ seen.add(sentence);
+ examples.push(createExample(sentence, translation));
+ }
}
if (examples.length >= 2) break;