- // ==UserScript==
- // @name 智能划词翻译工具
- // @namespace http://tampermonkey.net/
- // @version 1.0
- // @description 支持自动语言检测的划词翻译工具,带可视化界面,适配移动端居中显示
- // @author Ling
- // @match *://*/*
- // @connect fanyi.baidu.com
- // @grant GM_xmlhttpRequest
- // @grant GM_addStyle
- // @grant GM_notification
- // @description 2025/04/01 19:41:00
- // @license MIT
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- // 样式注入(优化移动端居中显示)
- GM_addStyle(`
- .translation-box {
- position: fixed;
- background: #ffffff;
- border: 1px solid #e0e0e0;
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
- padding: 16px;
- max-width: 90vw;
- width: 320px;
- z-index: 2147483647;
- font-family: 'Segoe UI', system-ui, sans-serif;
- transition: opacity 0.3s;
- box-sizing: border-box;
- }
- .translation-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12px;
- }
- .translation-title {
- font-weight: 600;
- color: #2d3748;
- font-size: 14px;
- }
- .translation-close {
- cursor: pointer;
- color: #718096;
- font-size: 18px;
- line-height: 1;
- padding: 4px;
- }
- .translation-content {
- line-height: 1.6;
- color: #4a5568;
- font-size: 14px;
- max-height: 50vh;
- overflow-y: auto;
- word-break: break-word;
- }
- .loading-indicator {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .loading-spinner {
- width: 16px;
- height: 16px;
- border: 2px solid #e2e8f0;
- border-top-color: #4299e1;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- }
- @keyframes spin {
- to { transform: rotate(360deg); }
- }
- @media (max-width: 768px) {
- .translation-box {
- width: 85vw;
- padding: 12px;
- font-size: 13px;
- left: 50%;
- transform: translateX(-50%);
- top: 20%; /* 移动端固定顶部20%位置 */
- }
- .translation-content {
- font-size: 13px;
- max-height: 40vh;
- }
- }
- `);
-
- // 翻译核心模块(保持不变)
- const TranslationCore = {
- async detectLanguage(text) {
- try {
- const response = await this._request({
- url: 'https://fanyi.baidu.com/langdetect',
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- data: `query=${encodeURIComponent(text)}`
- });
- if (response.error === 0 && response.lan) {
- return response.lan.toLowerCase();
- }
- throw new Error(response.msg || '检测失败');
- } catch (error) {
- console.warn('语言检测失败:', error);
- return 'auto';
- }
- },
-
- async translate(text, from = 'auto', to = 'zh') {
- try {
- if (from === 'auto') {
- from = await this.detectLanguage(text) || 'en';
- }
- if (from === 'zh' && to === 'auto') to = 'en';
- if (from !== 'zh' && to === 'auto') to = 'zh';
-
- const response = await this._request({
- url: 'https://fanyi.baidu.com/ait/text/translate',
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- data: JSON.stringify({
- query: text,
- from: from,
- to: to,
- milliTimestamp: Date.now(),
- domain: "common",
- needPhonetic: false
- })
- });
- return this._parseSSE(response);
- } catch (error) {
- console.error('翻译失败:', error);
- throw error;
- }
- },
-
- _parseSSE(rawData) {
- const events = rawData.split('\n\n').filter(Boolean);
- const results = [];
- for (const event of events) {
- const lines = event.split('\n');
- for (const line of lines) {
- if (line.startsWith('data:')) {
- try {
- const data = JSON.parse(line.slice(5).trim());
- if (data?.data?.event === 'Translating') {
- const valid = data.data.list
- .filter(item => item.dst?.trim())
- .map(item => item.dst);
- results.push(...valid);
- }
- } catch (e) {
- console.warn('SSE解析错误:', e);
- }
- }
- }
- }
- return results.length > 0 ? results.join('\n') : '未获取到有效翻译结果';
- },
-
- _request(options) {
- return new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- ...options,
- onload: (resp) => {
- try {
- resolve(JSON.parse(resp.responseText));
- } catch {
- resolve(resp.responseText);
- }
- },
- onerror: (err) => reject(err)
- });
- });
- }
- };
-
- // 用户界面控制器(调整定位逻辑)
- class TranslationUI {
- constructor() {
- this.isMobile = window.matchMedia('(max-width: 768px)').matches;
- this.initDOM();
- this.bindEvents();
- }
-
- initDOM() {
- this.container = document.createElement('div');
- this.container.className = 'translation-box';
- this.container.style.display = 'none';
- this.container.innerHTML = `
- <div class="translation-header">
- <span class="translation-title">智能翻译</span>
- <span class="translation-close">×</span>
- </div>
- <div class="translation-content"></div>
- `;
- document.body.appendChild(this.container);
- this.content = this.container.querySelector('.translation-content');
- this.closeButton = this.container.querySelector('.translation-close');
- }
-
- bindEvents() {
- this.closeButton.onclick = () => this.hide();
- document.addEventListener('mousedown', (e) => {
- if (!this.container.contains(e.target)) this.hide();
- });
- document.addEventListener('touchstart', (e) => {
- if (!this.container.contains(e.target)) this.hide();
- });
- }
-
- showLoading() {
- this.content.innerHTML = `
- <div class="loading-indicator">
- <div class="loading-spinner"></div>
- <span>翻译中...</span>
- </div>`;
- this.container.style.display = 'block';
- }
-
- showResult(text) {
- this.content.innerHTML = text;
- this.container.style.display = 'block';
- this.autoHide(5000);
- }
-
- showError(msg) {
- this.content.innerHTML = `<div style="color: #e53e3e;">${msg}</div>`;
- this.container.style.display = 'block';
- this.autoHide(3000);
- }
-
- hide() {
- this.container.style.display = 'none';
- }
-
- position(x, y) {
- if (this.isMobile) {
- // 移动端居中显示,CSS已处理水平居中,垂直位置固定为20%
- this.container.style.top = '20%';
- this.container.style.left = '50%';
- this.container.style.transform = 'translateX(-50%)';
- } else {
- // 桌面端基于鼠标/触摸位置
- const OFFSET = 15;
- const rect = this.container.getBoundingClientRect();
- let top = y + OFFSET;
- let left = x + OFFSET;
-
- if (left + rect.width > window.innerWidth) {
- left = Math.max(OFFSET, window.innerWidth - rect.width - OFFSET);
- }
- if (top + rect.height > window.innerHeight) {
- top = Math.max(OFFSET, y - rect.height - OFFSET);
- }
-
- this.container.style.top = `${top}px`;
- this.container.style.left = `${left}px`;
- this.container.style.transform = 'none'; // 清除移动端变换
- }
- }
-
- autoHide(delay) {
- clearTimeout(this.hideTimer);
- this.hideTimer = setTimeout(() => this.hide(), delay);
- }
- }
-
- function isInputElement(node) {
- return node && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA' || node.isContentEditable);
- }
-
- function isSearchBox(node) {
- return node && node.tagName === 'INPUT' && node.type === 'search';
- }
-
- let lastSelection = '';
- let lastTranslation = '';
- let cacheExpireTimer;
- const MAX_HISTORY = 15;
- let translationHistory = [];
-
- function updateCache(text, translation) {
- lastSelection = text;
- lastTranslation = translation;
- translationHistory = [
- { text, translation },
- ...translationHistory.slice(0, MAX_HISTORY - 1)
- ];
- clearTimeout(cacheExpireTimer);
- cacheExpireTimer = setTimeout(() => {
- lastSelection = '';
- lastTranslation = '';
- translationHistory = [];
- }, 1800000);
- }
-
- // 主程序
- (function init() {
- const ui = new TranslationUI();
- let debounceTimer = null;
-
- const debounce = (func, delay = 300) => {
- return (...args) => {
- clearTimeout(debounceTimer);
- debounceTimer = setTimeout(() => func.apply(this, args), delay);
- };
- };
-
- const handleTranslate = async (x, y, text) => {
- if (!text) return;
-
- const cached = translationHistory.find(item => item.text === text);
- if (cached) {
- ui.position(x, y);
- ui.showResult(cached.translation);
- return;
- }
-
- ui.currentText = text;
- ui.position(x, y);
- ui.showLoading();
-
- try {
- const result = await TranslationCore.translate(text);
- updateCache(text, result);
- ui.showResult(result);
- } catch (error) {
- ui.showError(`翻译失败: ${error.message || '服务不可用'}`);
- GM_notification({
- title: '翻译错误',
- text: error.message,
- timeout: 3000
- });
- }
- };
-
- const handleMouseUp = debounce((e) => {
- const selection = window.getSelection();
- const text = selection.toString().trim();
- if (text) handleTranslate(e.pageX, e.pageY, text);
- }, 150);
-
- const handleTouchEnd = debounce((e) => {
- const selection = window.getSelection();
- const text = selection.toString().trim();
- if (text) {
- const touch = e.changedTouches[0];
- handleTranslate(touch.pageX, touch.pageY, text);
- }
- }, 150);
-
- document.addEventListener('mouseup', handleMouseUp);
- document.addEventListener('touchend', handleTouchEnd);
- })();
- })();