翻译小工具 | 简单划选翻译 | 原文译文可展开对照,方便学习

页面翻译 | 选中文字( 按Ctrl )| 英文学习 | 翻译文可设置,支持全球多数通用语言 | 有什么问题都可以反馈

目前为 2024-05-21 提交的版本。查看 最新版本

// ==UserScript==
// @name         翻译小工具 | 简单划选翻译 | 原文译文可展开对照,方便学习
// @namespace    http://tampermonkey.net/
// @version      1.0.4
// @description  页面翻译 | 选中文字( 按Ctrl )| 英文学习 | 翻译文可设置,支持全球多数通用语言 | 有什么问题都可以反馈
// @author       [email protected]
// @match        *://*/*
// @license      GPLv3
// @icon         https://s21.ax1x.com/2024/05/17/pkuVzUH.png
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
// @grant        GM_xmlhttpRequest
// ==/UserScript==

var cssContent = `
.fy_btn_box{
    position: fixed;
    top: 50px;
    right: 50px;
    z-index: 9999;
    background-color: rgba(255, 255, 255, 0.5);
    border-radius: 10px;
    padding: 10px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
    color: #333;
}
#fy_transContainer{
    display: none;
    position: fixed;
    top: 50px;
    left: 50px;
    max-width: 300px;
    padding: 10px;
    padding-top: 24px;
    border-radius: 4px;
    flex-direction: column;
    justify-content: center;
    align-items: flex-start;
    box-shadow: 0 3px 10px 1px rgba(0, 0, 0, 0.1);
    background-color: rgba(255,255,255,0.7);
    backdrop-filter: saturate(420%) blur(50px);
    -webkit-backdrop-filter: saturate(420%) blur(50px);
    z-index: 9999;
    font-size: 14px;
    border-bottom-right-radius: 0;
    overflow: hidden;
}
#fy_dragBar{
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 20px;
    cursor: grab;
    // background-color: #F9E79F;
    background-color: rgba(0,0,0,0.1);
    backdrop-filter: saturate(420%) blur(50px);
    -webkit-backdrop-filter: saturate(420%) blur(50px);
}
#fy_Scale_rb{
    position:absolute;
    bottom:0;
    right:0;
    width:7px;
    height:7px;
    cursor: nw-resize;
    // background: #ddd;
    // clip-path: polygon(100% 0%, 100% 100%, 0% 100%);
}

#fy_contentBox{
    width: 100%;
    overflow: auto;
    line-height: 1.3em;
    letter-spacing: 0.5px;
}
#fy_contentBox .textRight{
    text-align: right;
}
.transText_node{
    width: 100%;
    padding: 7px;
    box-sizing: border-box;
}
.transText_node:hover{
    background-color: rgba(0,0,0,0.04);
    border-radius: 6px;
}
.transText_node_to{
    transition: all 0.2s;
}
.transText_node_from{
    height: 0;
    overflow: hidden;
    transition: all 0.2s;
}



#fy_contentBox .fy_node_expand{
    background-color: rgba(0,0,0,0.04);
    border-radius: 6px;
    margin: 5px 0;
}

.fy_node_expand .transText_node_from, .fy_node_expand .transText_node_to{
    padding: 6px 8px;
}
.fy_node_expand .transText_node_to{
    background-color: rgb(209, 255, 240);
    border-top-left-radius: 4px;
    border-top-right-radius: 4px;
}
.fy_node_expand .transText_node_from{
    background-color: rgb(254, 234, 242);
    height: auto;
    border-bottom-left-radius: 4px;
    border-bottom-right-radius: 4px;
}

.copy_icon{
    cursor: pointer;
    opacity: 0;
    transition: all 0.3s ease;
}
.transText_node_from:hover .copy_icon{
    opacity: 1;
}
.transText_node_to:hover .copy_icon{
    opacity: 1;
}


#fy_select{
    position: absolute;
    top: 2px;
    right: 0;
    background-color: transparent;
    border: none; /* 去除默认边框 */
    box-shadow: none; /* 去除默认的阴影 */
    outline: none; /* 去除可能的轮廓线 */
}
// .copy_icon{
//     position: absolute;
//     bottom: 10xp;
//     right: 10px;
// }
// #fy_top_tools{
//     width: auto;
//     position: relative;
//     display: inline-flex;
//     align-items: center;
//     box-sizing: border-box;
//     padding: 0 6px;
//     height: 100%;
//     cursor: initial;
// }
// .fy_top_toolItem{
//     padding: 3px 5px;
//     cursor: pointer;
//     display: flex;
//     align-items: center;
//     justify-content: center;
// }
// .tool_icon{
//     display: inline-block;
//     width: 10px;
//     height: 10px;
//     line-height: 9px;
//     border-radius: 10px;
//     font-size: 10px;
//     text-align: center;
//     background-color: red;
//     color: red;
// }
// .fy_top_toolItem:hover .tool_icon{
//     color:#fff;
// }



`


// ------------全局----------------
// 显示总容器
var transContainerDOM = null
// 翻译内容容器
var fyContentDOM = null
// 拖动条
var fyDragBarDOM = null
var fyScale_rb = null
// 待翻译文本
var fromTransTextArray = [];
// 翻译结果对象
var transRes = {}
// --------------------------
var transToTypes = [
    { type: 'zh', keyName: '中', name: '中文', isSelected: true },
    { type: 'en', keyName: '英', name: '英文', isSelected: false },
    { type: 'fra', keyName: '法', name: '法文', isSelected: false },
    { type: 'spa', keyName: '西', name: '西班牙文', isSelected: false },
    { type: 'jp', keyName: '日', name: '日文', isSelected: false },
    { type: 'kor', keyName: '韩', name: '韩文', isSelected: false },
    { type: 'ru', keyName: '俄', name: '俄文', isSelected: false },
    { type: 'de', keyName: '德', name: '德文', isSelected: false },
    { type: 'it', keyName: '意', name: '意大利文', isSelected: false },
    { type: 'th', keyName: '泰', name: '泰文', isSelected: false },
    { type: 'pt', keyName: '葡', name: '葡萄牙文', isSelected: false },
    { type: 'ara', keyName: '阿', name: '阿拉伯文', isSelected: false },
]

// 初始加载样式
const loadStyle = () => {
    var style = document.createElement('style');
    if (style.styleSheet) {
        // 对于老版本的IE浏览器
        style.styleSheet.cssText = cssContent;
    } else {
        style.appendChild(document.createTextNode(cssContent));
    }
    var head = document.head || document.getElementsByTagName('head')[0];
    head.appendChild(style);
}
/**
 * 传入配置信息创建元素并返回DOM对象
*/
const myCreateEle = (option, mountE) => {
    let e = document.createElement(option.el || 'div')
    option.className && e.classList.add(option.className)
    for (let p in (option.props || {})) {
        e.setAttribute(p, option.props[p])
    }
    e.innerText = option.text || ''
    e.style.cssText = option.style || ''
    mountE && mountE.appendChild(e)
    return e
}

// 初始生成元素
const initLoadElement = () => {
    transContainerDOM = myCreateEle({
        props: { id: 'fy_transContainer' }
    }, document.body)
    // 内容容器
    fyContentDOM = myCreateEle({ props: { id: 'fy_contentBox' } }, transContainerDOM)
    // loading
    $(transContainerDOM).append(`
    <div id="fy_loading">
        <svg viewBox="25 25 50 50">
            <circle r="20" cy="50" cx="50"></circle>
        </svg>
    </div>
    `)
    // 拖动条
    fyDragBarDOM = myCreateEle({
        props: { id: 'fy_dragBar' }
    }, transContainerDOM)
    // $(fyDragBarDOM).append(`
    //     <div id='fy_top_tools'>
    //         <div class='fy_top_toolItem'>
    //             <span class='tool_icon'>×</span>
    //         </div>
    //     </div>
    // `)
    // 缩放点-框
    fyScale_rb = myCreateEle({
        props: { id: 'fy_Scale_rb' }
    }, transContainerDOM)

    let optionsStr = ''
    transToTypes.forEach(item => {
        optionsStr += `
        <option value="${item.type}" title="${item.name}">
            <span >${item.keyName}</span>
        </option>
        `
    })
    // ❌➖📌💡🎯📝✔️❓❗️📅🚫🔄✅📖📘
    // filter: grayscale(100%); 置灰
    $(transContainerDOM).append(`
        <select id="fy_select">
        ${optionsStr}
        </select>
    `)


}


// 生成MD5值
const calculateMD5 = (input) => {
    return CryptoJS.MD5(input).toString();
}

// ---------------
var salt = Date.now()
var appid = '20240513002050392';
var fyToType = 'zh'
var currTransToObjs = []
// ---------------

// 处理翻译
const handleTranslate = async (fromTransText, reUpdate = false) => {
    var sign = calculateMD5(appid + fromTransText + salt + 'evAKKTnaxMEpHrnCxwDC');
    let param = `?q=${fromTransText}&from=auto&to=${fyToType}&appid=${appid}&salt=${salt}&sign=${sign}`

    // $(fyContentDOM).append(`
    //     <div class="transText_node">
    //         <div class="transText_node_to">${fromTransText}</div>
    //         <div class="transText_node_from">${fromTransText}</div>
    //     </div>
    // `)
    // $('#fy_loading').hide()
    // computedContainer()
    // console.log('~~ ', fyToType)
    // return
    await GM_xmlhttpRequest({
        url: "https://fanyi-api.baidu.com/api/trans/vip/translate" + param,
        method: "GET",
        onload: function (response) {
            if (response.status === 200) {
                let res = JSON.parse((response.responseText || ''))

                if (!(res.trans_result && res.trans_result.length > 0)) return;

                transRes = {
                    formLang: res.from,
                    toLang: res.to,
                    formText: res.trans_result[0].src,
                    toText: res.trans_result[0].dst,
                }
                currTransToObjs.push({ ...transRes })
                let textRight = fyToType === 'ara' ? 'textRight' : ''
                $(fyContentDOM).append(`
                    <div class="transText_node ${textRight}">
                        <div class="transText_node_to" title="${transRes.formText}">${transRes.toText} <span class="copy_icon" title="复制" value="${transRes.toText}">📝</span></div>
                        <div class="transText_node_from">${transRes.formText} <span class="copy_icon" title="复制" value="${transRes.formText}">📝</span></div>
                    </div>
                `)
                $('#fy_loading').hide()
                !reUpdate && computedContainer()
                // 复制翻译后文字
                copyText(transRes.toText)
            } else {
                console.error("Request failed with status111 " + response.status);
            }
        },
        onerror: function (e) {
            // <div>或是否在用VPN代理❗️(百度的api😅)</div>
            console.error("百度翻译请求失败: " + e.message);
            $(fyContentDOM).append(`
                <div class="transText_node" style="text-align: center;">
                    <div>请求失败❗️☹️</div>
                    <div>请检查网络连接❗️</div>
                </div>
            `)
            $('#fy_loading').hide()
        },
    });
}



var currX = 0; // 当前鼠标位置
var currY = 0; // 当前鼠标位置
var isContainer = false // 容器是否出现
var isCtrl = false; // 是否处于可翻译状态
// 绑定事件
const bingEvents = () => {
    // // transContainerDOM 翻译容器

    // 绑定Ctrl+右键点击翻译
    // bindCtrlRightClick()
    // 绑定只按下ctrl键
    bingOnlyCtrl()

    // 鼠标按下事件
    document.addEventListener("mouseup", function (event) {
        currX = event.clientX;
        currY = event.clientY;
    })

    // 点击页面
    document.body.onclick = function (event) {
        if (isContainer) {
            clearTransContainer()
        }
    };
    // 清除翻译容器
    const clearTransContainer = () => {
        isContainer = false
        transContainerDOM.style.display = 'none'
        fyContentDOM.innerText = ''
        transContainerDOM.style.maxWidth = '300px';
        transContainerDOM.style.width = 'auto'
        transContainerDOM.style.height = 'auto'
    }

    transContainerDOM.onclick = function (e) {
        e.stopPropagation(); // 阻止事件冒泡
    }
    // 上下文菜单
    document.addEventListener("contextmenu", function (event) {
        if (isCtrl) {
            // 取消默认行为(阻止上下文菜单出现)
            event.preventDefault();
        }
    });
    bindHandleDrag() // 绑定拖动模块事件
    bindHandleScale() // 绑定缩放模块事件
    bindHandleSelect() // 绑定切换翻译事件
    bindTextClick() // 点击翻译文本事件

}

// Ctrl + 鼠标右键点击事件
const bindCtrlRightClick = () => {
    document.addEventListener('keydown', (e) => {
        if (e.key === 'Control') {
            isCtrl = true;
        }
    });
    document.addEventListener('keyup', (e) => {
        if (e.key === 'Control') {
            isCtrl = false;
        }
    });
    // 鼠标按下事件
    document.addEventListener("mousedown", function (event) {
        currX = event.clientX;
        currY = event.clientY;
        if (isCtrl && event.button === 2) {
            // 获取Selection对象,选中的文本
            let textAll = window.getSelection().toString();
            if (!textAll) return
            startTrans(textAll)
        }
    })
}
// 只按下ctrl键 一直
const bingOnlyCtrl = () => {
    var isOnlyCtrl = false;
    document.addEventListener('keydown', function (event) {
        isOnlyCtrl = event.key === 'Control' ? true : false;
    });
    document.addEventListener('keyup', function (event) {
        if (isContainer) return;
        if (event.key === 'Control' && isOnlyCtrl) {
            let textAll = window.getSelection().toString();
            if (!textAll) return;
            startTrans(textAll)
        }
    });
}

const startTrans = (textAll) => {
    fromTransTextArray = formatTrans(textAll)
    isContainer = true
    transContainerDOM.style.display = 'flex'
    $('#fy_loading').show()
    fyContentDOM.innerText = ''
    computedContainer() // 计算容器位置
    fromTransTextArray.filter(text => text).forEach(text => {
        handleTranslate(text)
    })
}


// 切换翻译语言
const bindHandleSelect = () => {
    document.getElementById('fy_select').onchange = function (e) {
        fyToType = this.value
        fyContentDOM.innerText = ''
        $('#fy_loading').show()
        fromTransTextArray.filter(text => text).forEach(text => {
            handleTranslate(text, true)
        })
    }
}
// 拖动事件
const bindHandleDrag = () => {
    var isMove = false
    var mouseToEleX;
    var mouseToEleY;
    // 拖动处理
    fyDragBarDOM.addEventListener("mousedown", function (e) {
        if (!isCtrl) {
            isMove = true
            fyDragBarDOM.style.cursor = 'grabbing'
            // 获取鼠标相对于元素的位置
            mouseToEleX = e.clientX - transContainerDOM.getBoundingClientRect().left;
            mouseToEleY = e.clientY - transContainerDOM.getBoundingClientRect().top;
        }
    });
    // 当鼠标移动时
    window.addEventListener('mousemove', (e) => {
        if (!isMove) return
        // 防止默认的拖动选择文本行为
        e.preventDefault();
        let t = (e.clientY - mouseToEleY) < 0 ? 0 : e.clientY - mouseToEleY;
        // 更新元素的位置
        transContainerDOM.style.left = (e.clientX - mouseToEleX) + 'px';
        transContainerDOM.style.top = t + 'px';
    })
    // 当鼠标松开时
    window.addEventListener('mouseup', () => {
        isMove = false;
        fyDragBarDOM.style.cursor = 'grab'
    });
}
// 缩放事件
const bindHandleScale = () => {
    var mainCurrWidth;
    var mainCurrHeight;
    var cX, cY;
    var isScale = false;
    fyScale_rb.addEventListener('mousedown', (e) => {
        isScale = true
        mainCurrWidth = transContainerDOM.offsetWidth
        mainCurrHeight = transContainerDOM.offsetHeight
        cX = e.clientX;
        cY = e.clientY;
    });
    // 当鼠标移动时
    window.addEventListener('mousemove', (e) => {
        if (!isScale) return
        // 防止默认的拖动选择文本行为
        e.preventDefault();
        transContainerDOM.style.maxWidth = 'none'
        let newWidth = mainCurrWidth + (e.clientX - cX)
        let newHeight = mainCurrHeight + (e.clientY - cY)
        // 更新元素的位置
        transContainerDOM.style.width = Math.max(10, newWidth) + 'px';
        transContainerDOM.style.height = Math.max(10, newHeight) + 'px';
    })
    // 当鼠标松开时
    window.addEventListener('mouseup', () => {
        isScale = false;
        fyDragBarDOM.style.cursor = 'grab'
    });
}

// 点击译文事件
var isClickLock = true
const bindTextClick = () => {
    fyContentDOM.addEventListener('click', function (event) {
        if (!isClickLock) return;
        isClickLock = false
        setTimeout(() => {
            isClickLock = true;
        }, 300); // 双击事件的间隔时间通常是300毫秒左右
        let textAll = window.getSelection().toString();
        if (textAll) return;
        let targetEle = event.target
        if (!targetEle.classList.contains('transText_node')) {
            targetEle = targetEle.parentNode
        }
        if (!targetEle.classList.contains('transText_node')) return;

        if (targetEle.classList.contains('fy_node_expand')) {
            targetEle.classList.remove('fy_node_expand');
        } else {
            targetEle.classList.add('fy_node_expand')
        }

        var rect = transContainerDOM.getBoundingClientRect();
        // 获取视口的高度
        var viewportHeight = window.innerHeight || document.documentElement.clientHeight;
        // 计算元素底部到视口底部的距离
        if ((viewportHeight - rect.bottom) < 30) {
            transContainerDOM.style.height = (viewportHeight - rect.top - 50) + 'px'
        }
    });
    $(fyContentDOM).on('click', '.copy_icon', function () {
        copyText(this.getAttribute('value'))
        showMessage({
            message: '复制成功',
            time: 800,
        })
    })

}

// 计算渲染容器高度位置
const computedContainer = () => {
    let h_ = transContainerDOM.offsetHeight;
    let w_ = transContainerDOM.offsetWidth;
    let yinzi = 10
    let top = currY - h_ - yinzi;
    // 小窗口位置高过主窗口则在鼠标底部显示
    transContainerDOM.style.top = top < 1 ? (currY + yinzi) + 'px' : top + 'px';
    let left = currX - (w_ / 2);
    transContainerDOM.style.left = left < 1 ? '1px' : left + 'px'

    let topToBotton = window.innerHeight - currY
    if (transContainerDOM.offsetHeight > topToBotton) {
        transContainerDOM.style.height = topToBotton + 'px'
    }
}

// 语音播放文本
const playText = (text) => {
    // 检查浏览器是否支持语音合成
    if ('speechSynthesis' in window) {
        // 创建语音合成实例
        var synthesis = window.speechSynthesis;
        var textToSpeak = text;
        // 创建语音合成的配置
        var utterance = new SpeechSynthesisUtterance(textToSpeak);
        // 使用默认语音
        utterance.voice = speechSynthesis.getVoices()[0];
        // 播放文本
        synthesis.speak(utterance);
    } else {
        console.log("抱歉,您的浏览器不支持语音合成功能。");
    }
}

const init = (e) => {
    initLoadElement();
    bingEvents();
}

(function () {
    window.addEventListener("load", function () {
        loadStyle();
        this.setTimeout(() => {
            init()
        }, 300);
    })
})();














const showMessage = ({ type = 'success', message, time }) => {
    let tipsDOM = myCreateEle({ text: message, type: type ?? 'success', style: 'position: absolute; top: 30px; left: 50%; transform: translate(-50%, 0%); padding: 2px 6px; border-radius: 2px; color:#fff; background-color: #67c23a; font-size: 10px;' }, transContainerDOM)
    setTimeout(() => {
        tipsDOM.remove();
    }, time ?? 2000);
}


// 格式化页面划选的文本,拆分成数组
const formatTrans = (texts = '') => {
    return texts.split(/[\n\t]+/) || []
}

// copy 文本
const copyText = (text) => {
    navigator.clipboard.writeText(text)
        .then(() => {
            // console.log('文本已成功复制到剪贴板');

        })
        .catch(err => {
            // 某些浏览器可能不支持或需要用户交互  
            // console.error('无法复制文本: ', err);
        });
}





// -------------------------------------
cssContent += `

#fy_loading{
    display: none;
    width: 200px;
    padding-top: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
}
#fy_loading svg {
    width: 2.25em;
    transform-origin: center;
    animation: rotate4 2s linear infinite;
    }

#fy_loading circle {
    fill: none;
    stroke: hsl(214, 97%, 59%);
    stroke-width: 2;
    stroke-dasharray: 1, 200;
    stroke-dashoffset: 0;
    stroke-linecap: round;
    animation: dash4 1.5s ease-in-out infinite;
    }

@keyframes rotate4 {
    100% {
        transform: rotate(360deg);
    }
}

@keyframes dash4 {
    0% {
        stroke-dasharray: 1, 200;
        stroke-dashoffset: 0;
    }

    50% {
        stroke-dasharray: 90, 200;
        stroke-dashoffset: -35px;
    }

    100% {
        stroke-dashoffset: -125px;
    }
}



/* 滚动条整体样式 */
::-webkit-scrollbar {
    width: 6px;  /* 宽度 */
    height: 6px; /* 高度(对于垂直滚动条) */
}

/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
    background: #aaa;
    border-radius: 6px;
}

/* 滚动条滑块:hover状态样式 */
::-webkit-scrollbar-thumb:hover {
    background: #888;
}

/* 滚动条轨道 */
::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 6px;
}

/* 滚动条轨道:hover状态样式 */
::-webkit-scrollbar-track:hover {
    background: #ddd;
}

/* 滚动条轨道:active状态样式 */
::-webkit-scrollbar-track-piece:active {
    background: #eee;
}

/* 滚动条:角落样式(即两个滚动条交汇处) */
::-webkit-scrollbar-corner {
    background: #535353;
}
`

QingJ © 2025

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