/** * 网页翻译插件 * 提供整篇文章分段翻译功能 */ class WebTranslator { constructor(options = {}) { this.options = { targetLang: options.targetLang || 'en-US', sourceLang: options.sourceLang || 'auto', apiEndpoint: options.apiEndpoint || 'https://newagent.vsbclub.com/system/api/cloud/agent/v1/knowledge/commonPlugin', maxChunkLength: options.maxChunkLength || 5000, areas: options.areas || [], onTranslationStart: options.onTranslationStart || this._defaultOnTranslationStart.bind(this), onTranslationComplete: options.onTranslationComplete || this._defaultOnTranslationComplete.bind(this), onTranslationError: options.onTranslationError || this._defaultOnTranslationError.bind(this), ...options }; this.areaConfigs = new Map(); this.init(); } init() { this.initAreas(); this.createControls(); } languageSelectorId = 'languageSelector_vsb'; translateBtnId = 'translateAllBtn_vsb'; createControls() { // 创建语言选择器 const languageSelector = document.createElement('select'); languageSelector.id = this.languageSelectorId; languageSelector.className = 'language-selector'; // 添加语言选项 const languages = [ { value: 'en-US', text: 'English' }, { value: 'ja-JP', text: '日本語' }, { value: 'ko-KR', text: '한국어' }, { value: 'fr-FR', text: 'Français' }, { value: 'de-DE', text: 'Deutsch' }, { value: 'es-ES', text: 'Español' }, { value: 'ru-RU', text: 'Русский' }, { value: 'ar-SA', text: 'العربية' } ]; languages.forEach(lang => { const option = document.createElement('option'); option.value = lang.value; option.textContent = lang.text; if (lang.value === this.options.targetLang) { option.selected = true; } languageSelector.appendChild(option); }); // 创建翻译按钮 const translateBtn = document.createElement('button'); translateBtn.id = this.translateBtnId; translateBtn.type = 'button'; translateBtn.className = 'translate-btn'; translateBtn.textContent = '翻译'; // 设置事件监听 this._setupControlEvents(languageSelector, translateBtn); // 返回控制元素 return { languageSelector, translateBtn }; } _setupControlEvents(languageSelector, translateBtn) { // 语言选择器事件处理 languageSelector.addEventListener('change', (event) => { this.options.targetLang = event.target.value; translateBtn.textContent = '翻译'; translateBtn.disabled = false; translateBtn.dataset.hasTranslation = 'false'; translateBtn.dataset.isHidden = 'false'; this.clearAllTranslations(); }); // 翻译按钮事件处理 translateBtn.addEventListener('click', () => { const hasTranslation = translateBtn.dataset.hasTranslation === 'true'; const isHidden = translateBtn.dataset.isHidden === 'true'; if (hasTranslation) { const translatedElements = document.querySelectorAll('.translated-text'); if (isHidden) { translatedElements.forEach(el => el.style.display = ''); translateBtn.textContent = '隐藏翻译结果'; translateBtn.dataset.isHidden = 'false'; } else { translatedElements.forEach(el => el.style.display = 'none'); translateBtn.textContent = '显示翻译结果'; translateBtn.dataset.isHidden = 'true'; } } else { this.translateAll(); } }); } _defaultOnTranslationStart(areaId) { const btn = document.getElementById(this.translateBtnId); if (btn) { btn.textContent = '翻译中...'; btn.disabled = true; } } _defaultOnTranslationComplete(areaId) { const btn = document.getElementById(this.translateBtnId); if (btn) { btn.textContent = '隐藏翻译结果'; btn.disabled = false; btn.dataset.hasTranslation = 'true'; btn.dataset.isHidden = 'false'; } } _defaultOnTranslationError(areaId, error) { const btn = document.getElementById(this.translateBtnId); if (btn) { btn.textContent = '翻译'; btn.disabled = false; btn.dataset.hasTranslation = 'false'; btn.dataset.isHidden = 'false'; } console.error(`翻译区域 ${areaId} 出错:`, error); } initAreas() { // 初始化所有配置的翻译区域 this.options.areas.forEach((area, index) => { if (area.selector) { // 如果没有提供id,自动生成一个 const areaId = area.id || `area_${index}_${Date.now()}`; this.areaConfigs.set(areaId, { selector: area.selector, excludeSelectors: area.excludeSelectors || [] }); } }); } /** * 清除指定区域的所有翻译结果 * @param {string} selector - CSS选择器 */ clearTranslations(selector) { const element = document.querySelector(selector); if (!element) return; // 移除所有翻译结果和加载状态 const translations = element.querySelectorAll('.translated-text, .translation-loading, .translation-error'); translations.forEach(el => el.remove()); } /** * 清除所有区域的翻译结果 */ clearAllTranslations() { // 额外清空可能存在的其他翻译结果 document.querySelectorAll('.translated-text, .translation-loading, .translation-error').forEach(el => { el.remove(); }); } /** * 翻译所有配置的区域 * @returns {Promise} */ async translateAll() { // 在开始翻译前清空所有翻译结果 this.clearAllTranslations(); // 不再使用 Promise.all,而是逐个翻译区域 for (const [id, config] of this.areaConfigs.entries()) { this.translateArea(config.selector); } } /** * 翻译指定选择器对应的区域 * @param {string} selector - CSS选择器 * @returns {Promise} */ async translateArea(selector) { // 查找匹配的配置 let targetConfig = null; let targetId = null; for (const [id, config] of this.areaConfigs.entries()) { if (config.selector === selector) { targetConfig = config; targetId = id; break; } } if (!targetConfig) { console.error(`未找到选择器 ${selector} 对应的翻译区域配置`); return; } const element = document.querySelector(selector); if (!element) { console.error(`未找到选择器 ${selector} 对应的元素`); return; } try { this.options.onTranslationStart(targetId); await this._translateElement(element, targetConfig.excludeSelectors); this.options.onTranslationComplete(targetId); } catch (error) { this.options.onTranslationError(targetId, error); } } async _translateElement(element, excludeSelectors) { const paragraphs = this.extractParagraphs(element); if (!paragraphs.length) return; // 为每个段落创建翻译任务 const translationPromises = paragraphs.map(paragraph => { // 创建加载状态元素 const loadingElement = document.createElement('div'); loadingElement.className = 'translation-loading'; loadingElement.innerHTML = '
翻译中...'; paragraph.element.insertAdjacentElement('afterend', loadingElement); return this.translate(paragraph.text) .then(translatedText => { // 移除加载状态 loadingElement.remove(); // 立即显示翻译结果 this.insertTranslation(paragraph.element, translatedText); }) .catch(error => { // 移除加载状态并显示错误 loadingElement.remove(); console.error(`翻译段落失败: ${error.message}`); const errorElement = document.createElement('div'); errorElement.className = 'translation-error'; errorElement.textContent = '翻译失败,请重试'; paragraph.element.insertAdjacentElement('afterend', errorElement); }); }); // 等待所有翻译任务完成 await Promise.all(translationPromises); } extractParagraphs(element) { // 查找所有段落 const paragraphs = Array.from(element.querySelectorAll('p')) .filter(p => !p.classList.contains('vsbcontent_img')) .map(p => ({ element: p, text: p.innerText.trim() })) .filter(item => item.text.length > 0); // 如果没有找到段落,直接翻译整个元素的内容 if (paragraphs.length === 0) { const text = element.innerText.trim(); if (text.length > 0) { return [{ element: element, text: text }]; } } return paragraphs; } async translate(text) { return new Promise((resolve, reject) => { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); // 30秒超时 const requestBody = { query: { content: text, targetLang: this.options.targetLang }, type: "translation" }; fetch(this.options.apiEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(requestBody), signal: controller.signal }) .then(response => { clearTimeout(timeoutId); if (!response.ok) { return response.text().then(errorText => { throw new Error(`翻译请求失败: ${response.status} ${response.statusText}\n响应内容: ${errorText}`); }); } return response.json(); }) .then(data => { if (!data || !data.data) { throw new Error('翻译服务返回数据格式错误'); } resolve(data.data); }) .catch(error => { if (error.name === 'AbortError') { reject(new Error('翻译请求超时,请稍后重试')); } else { reject(error); } }); } catch (error) { reject(error); } }); } /** * 插入翻译结果 * @param {HTMLElement} originalElement - 原始元素 * @param {string} translatedText - 翻译后的文本 */ insertTranslation(originalElement, translatedText) { // 创建翻译元素,保持与原始元素相同的标签类型 const translationElement = document.createElement(originalElement.tagName.toLowerCase()); translationElement.className = 'translated-text'; translationElement.textContent = translatedText; // 复制原始元素的所有内联样式 const originalStyle = window.getComputedStyle(originalElement); const styleProperties = [ 'font-family', 'font-size', 'font-weight', 'font-style', 'line-height', 'color', 'background-color', 'margin', 'padding', 'text-align', 'text-indent', 'letter-spacing', 'word-spacing', 'white-space' ]; styleProperties.forEach(prop => { translationElement.style[prop] = originalStyle.getPropertyValue(prop); }); // 在原文后插入翻译 originalElement.insertAdjacentElement('afterend', translationElement); } } // 将WebTranslator类注册为全局变量 window.WebTranslator = WebTranslator;