// ==UserScript== // @name 能不能好好说话?美化版 // @namespace https://lab.magiconch.com/nbnhhsh // @version 0.16.1 // @description 首字母缩写划词翻译工具 - 兼容性优化版 // @author itorr (美化 by Ace) // @license MIT // @icon https://lab.magiconch.com/favicon.ico // @match *://weibo.com/* // @match *://*.weibo.com/* // @match *://*.weibo.cn/* // @match *://tieba.baidu.com/* // @match *://*.bilibili.com/ // @match *://*.bilibili.com/* // @match *://*.douban.com/group/* // @match *://*.xiaoheihe.cn/* // @match *://xiaoheihe.cn/* // @require https://lab.magiconch.com/vue.2.6.11.min.js // @inject-into content // @grant GM_addStyle // ==/UserScript== let Nbnhhsh = ((htmlText, cssText) => { const API_URL = 'https://lab.magiconch.com/api/nbnhhsh/'; // 使用GM_addStyle注入CSS,避免样式冲突 GM_addStyle(cssText); const request = (method, url, data, onOver) => { let x = new XMLHttpRequest(); x.open(method, url); x.setRequestHeader('content-type', 'application/json'); x.withCredentials = true; x.onload = () => onOver(x.responseText ? JSON.parse(x.responseText) : null); x.send(JSON.stringify(data)); return x; }; const Guess = {}; const guess = (text, onOver) => { text = text.match(/[a-z0-9]{2,}/ig).join(','); if (Guess[text]) { return onOver(Guess[text]); } if (guess._request) { guess._request.abort(); } app.loading = true; guess._request = request('POST', API_URL + 'guess', { text }, data => { Guess[text] = data; onOver(data); app.loading = false; }); }; // 更健壮的模态框实现 const createModal = (name, callback) => { // 创建模态框容器 const modal = document.createElement('div'); modal.className = 'nbnhhsh-modal'; modal.setAttribute('data-nbnhhsh-modal', ''); // 添加自定义属性便于识别 // 创建shadow DOM隔离样式 let shadowRoot; try { shadowRoot = modal.attachShadow({ mode: 'open' }); } catch (e) { shadowRoot = modal; // 回退方案 } // 模态框内容 shadowRoot.innerHTML = ` `; // 添加到body最外层 document.documentElement.appendChild(modal); // 获取DOM元素 const overlay = shadowRoot.querySelector('.modal-overlay'); const closeBtn = shadowRoot.querySelector('.close-btn'); const cancelBtn = shadowRoot.querySelector('.cancel-btn'); const submitBtn = shadowRoot.querySelector('.submit-btn'); const input = shadowRoot.querySelector('.modal-input'); // 关闭模态框函数 const closeModal = () => { modal.style.opacity = '0'; setTimeout(() => { if (modal.parentNode) { modal.parentNode.removeChild(modal); } }, 200); }; // 事件监听 const handleSubmit = () => { const text = input.value.trim(); if (text) { closeModal(); callback(text); // 显示提交成功的提示 showNotice('感谢您的贡献!翻译提交成功,审核通过后生效。'); } else { input.focus(); } }; const handleKeyDown = (e) => { if (e.key === 'Escape') { closeModal(); } else if (e.key === 'Enter' && e.ctrlKey) { handleSubmit(); } }; overlay.addEventListener('click', closeModal); closeBtn.addEventListener('click', closeModal); cancelBtn.addEventListener('click', closeModal); submitBtn.addEventListener('click', handleSubmit); input.addEventListener('keydown', handleKeyDown); document.addEventListener('keydown', handleKeyDown); // 显示动画 setTimeout(() => { modal.style.opacity = '1'; input.focus(); }, 10); // 清理事件监听 modal._cleanup = () => { document.removeEventListener('keydown', handleKeyDown); }; }; // 显示通知 const showNotice = (message) => { const notice = document.createElement('div'); notice.className = 'nbnhhsh-notice'; notice.setAttribute('data-nbnhhsh-notice', ''); notice.textContent = message; // 添加到body最外层 document.documentElement.appendChild(notice); setTimeout(() => { notice.style.opacity = '0'; setTimeout(() => { if (notice.parentNode) { notice.parentNode.removeChild(notice); } }, 300); }, 3000); }; const submitTran = name => { createModal(name, text => { request('POST', API_URL + 'translation/' + name, { text }, () => { showNotice('感谢您的贡献!翻译提交成功,审核通过后生效。'); }); }); }; const transArrange = trans => { return trans.map(tran => { const match = tran.match(/^(.+?)([(\(](.+?)[)\)])?$/); if (match.length === 4) { return { text: match[1], sub: match[3] } } else { return { text: tran } } }) }; const getSelectionText = _ => { let text = getSelection().toString().trim(); if (!!text && /[a-z0-9]/i.test(text)) { return text; } else { return null; } }; const fixPosition = _ => { let rect = getSelection().getRangeAt(0).getBoundingClientRect(); const activeEl = document.activeElement; if (['TEXTAREA', 'INPUT'].includes(activeEl.tagName)) rect = activeEl.getBoundingClientRect(); let scrollTop = document.documentElement.scrollTop || document.body.scrollTop; let scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; let top = Math.floor(scrollTop + rect.top + rect.height + 5); let left = Math.floor(scrollLeft + rect.left); // 防止弹出框超出屏幕右侧 const boxWidth = 360; if (left + boxWidth > window.innerWidth) { left = window.innerWidth - boxWidth - 10; } // 防止弹出框超出屏幕底部 const estimatedHeight = 200; if (top + estimatedHeight > window.innerHeight + scrollTop) { top = Math.floor(scrollTop + rect.top - estimatedHeight - 5); if (top < scrollTop) top = scrollTop; } if (top === 0 && left === 0) { app.show = false; } app.top = top; app.left = left; }; const timer = _ => { if (getSelectionText()) { setTimeout(timer, 300); } else { app.show = false; } }; const nbnhhsh = _ => { let text = getSelectionText(); app.show = !!text && /[a-z0-9]/i.test(text); if (!app.show) { return; } fixPosition(); guess(text, data => { if (!data || !data.length) { app.show = false; } else { app.tags = data; } }); setTimeout(timer, 300); }; const _nbnhhsh = _ => { setTimeout(nbnhhsh, 1); }; document.body.addEventListener('mouseup', _nbnhhsh); document.body.addEventListener('keyup', _nbnhhsh); const createEl = html => { createEl._el.innerHTML = html; let el = createEl._el.children[0]; document.body.appendChild(el); return el; }; createEl._el = document.createElement('div'); // 创建主界面 const el = createEl(htmlText); const app = new Vue({ el, data: { tags: [], show: false, loading: false, top: 0, left: 0, }, methods: { submitTran, transArrange, } }); return { guess, submitTran, transArrange, } })(`
正在查询...

{{tag.name}}

{{tran.text}}{{tran.sub}}
无对应翻译
可能的含义:
{{input}}
提交翻译
`, ` .nbnhhsh-box { font: 400 14px/1.5 'PingFang SC', 'Microsoft YaHei', sans-serif; color: #333; border-radius: 8px; overflow: hidden; transition: all 0.2s ease; max-width: 95vw; } .nbnhhsh-box-pop { position: absolute; z-index: 2147483647; width: 360px; background: #FFF; box-shadow: 0 6px 30px -10px rgba(0, 0, 0, 0.2); margin: 10px 0; border: 1px solid rgba(0, 0, 0, 0.08); animation: fadeIn 0.15s ease-out; } @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } } .nbnhhsh-box-pop::before { content: ''; position: absolute; top: -8px; left: 20px; width: 0; height: 0; border: 8px solid transparent; border-bottom-color: #FFF; filter: drop-shadow(0 -2px 1px rgba(0, 0, 0, 0.05)); } .nbnhhsh-box sub { vertical-align: baseline; background: rgba(0, 99, 255, 0.1); color: #666; font-size: 12px; line-height: 1.4; display: inline-block; padding: 0 4px; margin-left: 4px; border-radius: 4px; letter-spacing: -0.5px; } .nbnhhsh-tag-list { max-height: 60vh; overflow-y: auto; scrollbar-width: thin; scrollbar-color: rgba(0, 99, 255, 0.3) transparent; } .nbnhhsh-tag-list::-webkit-scrollbar { width: 6px; } .nbnhhsh-tag-list::-webkit-scrollbar-thumb { background-color: rgba(0, 99, 255, 0.3); border-radius: 3px; } .nbnhhsh-tag-list::-webkit-scrollbar-track { background: transparent; } .nbnhhsh-tag-item { padding: 12px 16px; position: relative; border-bottom: 1px solid rgba(0, 0, 0, 0.05); } .nbnhhsh-tag-item:last-child { border-bottom: none; } .tag-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .nbnhhsh-tag-item h4 { font-weight: 600; font-size: 18px; line-height: 1.3; letter-spacing: 0.5px; margin: 0; color: #0063ff; font-family: 'Courier New', monospace; } .nbnhhsh-tran-list { color: #333; line-height: 1.8; display: flex; flex-wrap: wrap; gap: 10px 16px; margin-top: 8px; } .nbnhhsh-tran-item { display: inline-block; padding: 20px 24px; margin: 12px; position: relative; background: rgba(0, 99, 255, 0.05); border-radius: 6px; transition: all 0.2s; } .nbnhhsh-tran-item:hover { background: rgba(0, 99, 255, 0.1); color: #0063ff; } .nbnhhsh-inputting-list { color: #222; padding: 8px 12px; } .nbnhhsh-inputting-list h5 { font-size: 12px; line-height: 1.5; color: #888; margin: 0 0 6px 0; font-weight: 500; } .inputting-items { display: flex; flex-wrap: wrap; gap: 8px; } .nbnhhsh-inputting-item { background: rgba(0, 99, 255, 0.1); color: #0063ff; padding: 4px 8px; border-radius: 4px; font-size: 13px; line-height: 1.4; } .nbnhhsh-notran-box { padding: 8px 0; color: #888; display: flex; align-items: center; gap: 6px; font-size: 13px; } .nbnhhsh-notran-box.clickable { cursor: pointer; transition: color 0.2s; } .nbnhhsh-notran-box.clickable:hover { color: #0063ff; } .nbnhhsh-loading { text-align: center; color: #888; padding: 20px 0; display: flex; flex-direction: column; align-items: center; gap: 8px; } .spinner { width: 24px; height: 24px; border: 3px solid rgba(0, 99, 255, 0.2); border-top-color: #0063ff; border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .nbnhhsh-add-btn { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; color: #0063ff; cursor: pointer; transition: all 0.2s; border-radius: 4px; background: rgba(0, 99, 255, 0.1); } .nbnhhsh-add-btn:hover { background: rgba(0, 99, 255, 0.2); } .nbnhhsh-add-btn svg { transition: transform 0.2s; } .nbnhhsh-add-btn:hover svg { transform: scale(1.1); } /* 通知样式 */ [data-nbnhhsh-notice] { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.8); color: white; padding: 12px 24px; border-radius: 6px; font-size: 14px; z-index: 2147483647; opacity: 1; transition: opacity 0.3s ease; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important; } @media (max-width: 480px) { .nbnhhsh-box-pop { width: 280px; } .nbnhhsh-tag-item { padding: 10px 12px; } .nbnhhsh-tag-item h4 { font-size: 16px; } [data-nbnhhsh-notice] { width: 90%; text-align: center; } } `);