智能划词翻译工具

支持自动语言检测的划词翻译工具,带可视化界面,适配移动端居中显示

  1. // ==UserScript==
  2. // @name 智能划词翻译工具
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description 支持自动语言检测的划词翻译工具,带可视化界面,适配移动端居中显示
  6. // @author Ling
  7. // @match *://*/*
  8. // @connect fanyi.baidu.com
  9. // @grant GM_xmlhttpRequest
  10. // @grant GM_addStyle
  11. // @grant GM_notification
  12. // @description 2025/04/01 19:41:00
  13. // @license MIT
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. // 样式注入(优化移动端居中显示)
  20. GM_addStyle(`
  21. .translation-box {
  22. position: fixed;
  23. background: #ffffff;
  24. border: 1px solid #e0e0e0;
  25. border-radius: 8px;
  26. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  27. padding: 16px;
  28. max-width: 90vw;
  29. width: 320px;
  30. z-index: 2147483647;
  31. font-family: 'Segoe UI', system-ui, sans-serif;
  32. transition: opacity 0.3s;
  33. box-sizing: border-box;
  34. }
  35. .translation-header {
  36. display: flex;
  37. justify-content: space-between;
  38. align-items: center;
  39. margin-bottom: 12px;
  40. }
  41. .translation-title {
  42. font-weight: 600;
  43. color: #2d3748;
  44. font-size: 14px;
  45. }
  46. .translation-close {
  47. cursor: pointer;
  48. color: #718096;
  49. font-size: 18px;
  50. line-height: 1;
  51. padding: 4px;
  52. }
  53. .translation-content {
  54. line-height: 1.6;
  55. color: #4a5568;
  56. font-size: 14px;
  57. max-height: 50vh;
  58. overflow-y: auto;
  59. word-break: break-word;
  60. }
  61. .loading-indicator {
  62. display: flex;
  63. align-items: center;
  64. gap: 8px;
  65. }
  66. .loading-spinner {
  67. width: 16px;
  68. height: 16px;
  69. border: 2px solid #e2e8f0;
  70. border-top-color: #4299e1;
  71. border-radius: 50%;
  72. animation: spin 1s linear infinite;
  73. }
  74. @keyframes spin {
  75. to { transform: rotate(360deg); }
  76. }
  77. @media (max-width: 768px) {
  78. .translation-box {
  79. width: 85vw;
  80. padding: 12px;
  81. font-size: 13px;
  82. left: 50%;
  83. transform: translateX(-50%);
  84. top: 20%; /* 移动端固定顶部20%位置 */
  85. }
  86. .translation-content {
  87. font-size: 13px;
  88. max-height: 40vh;
  89. }
  90. }
  91. `);
  92.  
  93. // 翻译核心模块(保持不变)
  94. const TranslationCore = {
  95. async detectLanguage(text) {
  96. try {
  97. const response = await this._request({
  98. url: 'https://fanyi.baidu.com/langdetect',
  99. method: 'POST',
  100. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  101. data: `query=${encodeURIComponent(text)}`
  102. });
  103. if (response.error === 0 && response.lan) {
  104. return response.lan.toLowerCase();
  105. }
  106. throw new Error(response.msg || '检测失败');
  107. } catch (error) {
  108. console.warn('语言检测失败:', error);
  109. return 'auto';
  110. }
  111. },
  112.  
  113. async translate(text, from = 'auto', to = 'zh') {
  114. try {
  115. if (from === 'auto') {
  116. from = await this.detectLanguage(text) || 'en';
  117. }
  118. if (from === 'zh' && to === 'auto') to = 'en';
  119. if (from !== 'zh' && to === 'auto') to = 'zh';
  120.  
  121. const response = await this._request({
  122. url: 'https://fanyi.baidu.com/ait/text/translate',
  123. method: 'POST',
  124. headers: { 'Content-Type': 'application/json' },
  125. data: JSON.stringify({
  126. query: text,
  127. from: from,
  128. to: to,
  129. milliTimestamp: Date.now(),
  130. domain: "common",
  131. needPhonetic: false
  132. })
  133. });
  134. return this._parseSSE(response);
  135. } catch (error) {
  136. console.error('翻译失败:', error);
  137. throw error;
  138. }
  139. },
  140.  
  141. _parseSSE(rawData) {
  142. const events = rawData.split('\n\n').filter(Boolean);
  143. const results = [];
  144. for (const event of events) {
  145. const lines = event.split('\n');
  146. for (const line of lines) {
  147. if (line.startsWith('data:')) {
  148. try {
  149. const data = JSON.parse(line.slice(5).trim());
  150. if (data?.data?.event === 'Translating') {
  151. const valid = data.data.list
  152. .filter(item => item.dst?.trim())
  153. .map(item => item.dst);
  154. results.push(...valid);
  155. }
  156. } catch (e) {
  157. console.warn('SSE解析错误:', e);
  158. }
  159. }
  160. }
  161. }
  162. return results.length > 0 ? results.join('\n') : '未获取到有效翻译结果';
  163. },
  164.  
  165. _request(options) {
  166. return new Promise((resolve, reject) => {
  167. GM_xmlhttpRequest({
  168. ...options,
  169. onload: (resp) => {
  170. try {
  171. resolve(JSON.parse(resp.responseText));
  172. } catch {
  173. resolve(resp.responseText);
  174. }
  175. },
  176. onerror: (err) => reject(err)
  177. });
  178. });
  179. }
  180. };
  181.  
  182. // 用户界面控制器(调整定位逻辑)
  183. class TranslationUI {
  184. constructor() {
  185. this.isMobile = window.matchMedia('(max-width: 768px)').matches;
  186. this.initDOM();
  187. this.bindEvents();
  188. }
  189.  
  190. initDOM() {
  191. this.container = document.createElement('div');
  192. this.container.className = 'translation-box';
  193. this.container.style.display = 'none';
  194. this.container.innerHTML = `
  195. <div class="translation-header">
  196. <span class="translation-title">智能翻译</span>
  197. <span class="translation-close">×</span>
  198. </div>
  199. <div class="translation-content"></div>
  200. `;
  201. document.body.appendChild(this.container);
  202. this.content = this.container.querySelector('.translation-content');
  203. this.closeButton = this.container.querySelector('.translation-close');
  204. }
  205.  
  206. bindEvents() {
  207. this.closeButton.onclick = () => this.hide();
  208. document.addEventListener('mousedown', (e) => {
  209. if (!this.container.contains(e.target)) this.hide();
  210. });
  211. document.addEventListener('touchstart', (e) => {
  212. if (!this.container.contains(e.target)) this.hide();
  213. });
  214. }
  215.  
  216. showLoading() {
  217. this.content.innerHTML = `
  218. <div class="loading-indicator">
  219. <div class="loading-spinner"></div>
  220. <span>翻译中...</span>
  221. </div>`;
  222. this.container.style.display = 'block';
  223. }
  224.  
  225. showResult(text) {
  226. this.content.innerHTML = text;
  227. this.container.style.display = 'block';
  228. this.autoHide(5000);
  229. }
  230.  
  231. showError(msg) {
  232. this.content.innerHTML = `<div style="color: #e53e3e;">${msg}</div>`;
  233. this.container.style.display = 'block';
  234. this.autoHide(3000);
  235. }
  236.  
  237. hide() {
  238. this.container.style.display = 'none';
  239. }
  240.  
  241. position(x, y) {
  242. if (this.isMobile) {
  243. // 移动端居中显示,CSS已处理水平居中,垂直位置固定为20%
  244. this.container.style.top = '20%';
  245. this.container.style.left = '50%';
  246. this.container.style.transform = 'translateX(-50%)';
  247. } else {
  248. // 桌面端基于鼠标/触摸位置
  249. const OFFSET = 15;
  250. const rect = this.container.getBoundingClientRect();
  251. let top = y + OFFSET;
  252. let left = x + OFFSET;
  253.  
  254. if (left + rect.width > window.innerWidth) {
  255. left = Math.max(OFFSET, window.innerWidth - rect.width - OFFSET);
  256. }
  257. if (top + rect.height > window.innerHeight) {
  258. top = Math.max(OFFSET, y - rect.height - OFFSET);
  259. }
  260.  
  261. this.container.style.top = `${top}px`;
  262. this.container.style.left = `${left}px`;
  263. this.container.style.transform = 'none'; // 清除移动端变换
  264. }
  265. }
  266.  
  267. autoHide(delay) {
  268. clearTimeout(this.hideTimer);
  269. this.hideTimer = setTimeout(() => this.hide(), delay);
  270. }
  271. }
  272.  
  273. function isInputElement(node) {
  274. return node && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA' || node.isContentEditable);
  275. }
  276.  
  277. function isSearchBox(node) {
  278. return node && node.tagName === 'INPUT' && node.type === 'search';
  279. }
  280.  
  281. let lastSelection = '';
  282. let lastTranslation = '';
  283. let cacheExpireTimer;
  284. const MAX_HISTORY = 15;
  285. let translationHistory = [];
  286.  
  287. function updateCache(text, translation) {
  288. lastSelection = text;
  289. lastTranslation = translation;
  290. translationHistory = [
  291. { text, translation },
  292. ...translationHistory.slice(0, MAX_HISTORY - 1)
  293. ];
  294. clearTimeout(cacheExpireTimer);
  295. cacheExpireTimer = setTimeout(() => {
  296. lastSelection = '';
  297. lastTranslation = '';
  298. translationHistory = [];
  299. }, 1800000);
  300. }
  301.  
  302. // 主程序
  303. (function init() {
  304. const ui = new TranslationUI();
  305. let debounceTimer = null;
  306.  
  307. const debounce = (func, delay = 300) => {
  308. return (...args) => {
  309. clearTimeout(debounceTimer);
  310. debounceTimer = setTimeout(() => func.apply(this, args), delay);
  311. };
  312. };
  313.  
  314. const handleTranslate = async (x, y, text) => {
  315. if (!text) return;
  316.  
  317. const cached = translationHistory.find(item => item.text === text);
  318. if (cached) {
  319. ui.position(x, y);
  320. ui.showResult(cached.translation);
  321. return;
  322. }
  323.  
  324. ui.currentText = text;
  325. ui.position(x, y);
  326. ui.showLoading();
  327.  
  328. try {
  329. const result = await TranslationCore.translate(text);
  330. updateCache(text, result);
  331. ui.showResult(result);
  332. } catch (error) {
  333. ui.showError(`翻译失败: ${error.message || '服务不可用'}`);
  334. GM_notification({
  335. title: '翻译错误',
  336. text: error.message,
  337. timeout: 3000
  338. });
  339. }
  340. };
  341.  
  342. const handleMouseUp = debounce((e) => {
  343. const selection = window.getSelection();
  344. const text = selection.toString().trim();
  345. if (text) handleTranslate(e.pageX, e.pageY, text);
  346. }, 150);
  347.  
  348. const handleTouchEnd = debounce((e) => {
  349. const selection = window.getSelection();
  350. const text = selection.toString().trim();
  351. if (text) {
  352. const touch = e.changedTouches[0];
  353. handleTranslate(touch.pageX, touch.pageY, text);
  354. }
  355. }, 150);
  356.  
  357. document.addEventListener('mouseup', handleMouseUp);
  358. document.addEventListener('touchend', handleTouchEnd);
  359. })();
  360. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址