Greasy Fork

Greasy Fork is available in English.

禁用电商平台图片缩放效果

禁用淘宝、天猫、1688等电商平台的js-image-zoom效果,方便右键保存图片。

当前为 2025-05-28 提交的版本,查看 最新版本

// ==UserScript==
// @name             禁用电商平台图片缩放效果
// @name:en          Disable E-commerce Image Zoom Effect
// @namespace        http://greasyfork.icu/users/3001-hanjian-wu
// @version          1.3.1
// @description      禁用淘宝、天猫、1688等电商平台的js-image-zoom效果,方便右键保存图片。
// @description:en   Disable js-image-zoom effect on Taobao, Tmall, 1688 and other e-commerce platforms for easy right-click image saving. Fully fixed right-click direct access to images
// @author           hanjian wu
// @homepage         http://greasyfork.icu/users/3001-hanjian-wu
// @supportURL       http://greasyfork.icu/users/3001-hanjian-wu
// @license          MIT
// @match            https://*.taobao.com/item.htm*
// @match            https://*.tmall.com/item.htm*
// @match            https://*.1688.com/offer/*.html*
// @match            https://item.taobao.com/*
// @match            https://detail.tmall.com/*
// @match            https://detail.1688.com/*
// @icon             data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjY0IiByeD0iOCIgZmlsbD0iIzAwN0FGRiIvPgo8cGF0aCBkPSJNMTYgMTZIMzJWMzJIMTZWMTZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMzYgMTZINDhWMjhIMzZWMTZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTYgMzZIMjhWNDhIMTZWMzZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMzIgMzJINDhWNDhIMzJWMzJaIiBmaWxsPSJ3aGl0ZSIgZmlsbC1vcGFjaXR5PSIwLjciLz4KPC9zdmc+
// @grant            none
// @run-at           document-start
// @noframes
// @compatible       chrome
// @compatible       firefox
// @compatible       edge
// @compatible       safari
// ==/UserScript==

(function() {
    'use strict';

    console.log('电商图片缩放禁用脚本已启动 v1.3.1 (修复版)');

    // 检测当前平台
    const is1688 = window.location.hostname.includes('1688.com');
    // const isTaobao = window.location.hostname.includes('taobao.com'); // 已被 isTaobaoTmallCommon 替代
    // const isTmall = window.location.hostname.includes('tmall.com'); // 已被 isTaobaoTmallCommon 替代
    const isTaobaoTmallCommon = window.location.hostname.includes('taobao.com') || window.location.hostname.includes('tmall.com');


    let zoomEventBlocked = false;

    // 禁用图片缩放的主要函数
    function disableImageZoom() {
        if (is1688) {
            handle1688Zoom();
        } else if (isTaobaoTmallCommon) { // 使用合并后的检测
            handleTaobaoTmallZoom();
        }
    }

    // 处理1688的缩放
    function handle1688Zoom() {
        const zoomElements = document.querySelectorAll('.scale-img, .scaled-img');
        zoomElements.forEach(el => {
            if (el.style.display !== 'none') {
                el.style.display = 'none !important';
                el.style.visibility = 'hidden !important';
                el.style.opacity = '0 !important';
                el.style.pointerEvents = 'none !important';
            }
        });

        // 确保主图片可以右键
        const previewImg = document.querySelector('.detail-gallery-preview .preview-img');
        if (previewImg) {
            enableImageRightClick(previewImg);
        }
    }

    // 处理淘宝/天猫的缩放 - 重点修复右键问题
    function handleTaobaoTmallZoom() {
        // 隐藏缩放相关元素
        const zoomElements = document.querySelectorAll('.js-image-zoom__zoomed-area, .js-image-zoom__zoomed-image, #lensDiv');
        zoomElements.forEach(el => {
            el.style.display = 'none !important';
            el.style.visibility = 'hidden !important';
            el.style.opacity = '0 !important';
            el.style.pointerEvents = 'none !important';
        });

        // 关键修复:让容器的鼠标事件穿透到图片
        // 使用属性选择器 [class*="--mainPicWrap--"] 来匹配动态类名
        const mainPicWrappers = document.querySelectorAll('[class*="--mainPicWrap--"]');
        mainPicWrappers.forEach(wrapper => {
            // 让容器的鼠标事件穿透
            wrapper.style.pointerEvents = 'none';

            // 找到其中的图片并确保可以接收鼠标事件
            // 使用属性选择器 [class*="--mainPic--"]
            const mainPic = wrapper.querySelector('[class*="--mainPic--"]');
            if (mainPic) {
                // 让图片重新获得鼠标事件
                mainPic.style.pointerEvents = 'auto';
                mainPic.style.position = 'relative'; // 确保 z-index 生效
                mainPic.style.zIndex = '999'; // 提升层级
                enableImageRightClick(mainPic);

                console.log('已修复图片右键功能:', mainPic.src);
            }
        });

        // 确保缩略图可以点击切换
        // 使用属性选择器 [class*="--thumbnail--"] 和 [class*="--thumbnailPic--"]
        const thumbnails = document.querySelectorAll('[class*="--thumbnail--"]');
        thumbnails.forEach(thumb => {
            thumb.style.pointerEvents = 'auto';
            const thumbImg = thumb.querySelector('[class*="--thumbnailPic--"]');
            if (thumbImg) {
                thumbImg.style.pointerEvents = 'auto';
            }
        });

        // 确保切换标签可以点击
        // 使用属性选择器 [class*="--switchTabsItem--"]
        const switchTabs = document.querySelectorAll('[class*="--switchTabsItem--"]');
        switchTabs.forEach(tab => {
            tab.style.pointerEvents = 'auto';
        });
    }

    // 启用图片右键功能
    function enableImageRightClick(img) {
        if (!img) return;

        // 确保图片可以接收鼠标事件
        img.style.pointerEvents = 'auto';
        img.style.userSelect = 'auto';
        img.style.webkitUserSelect = 'auto';
        img.style.mozUserSelect = 'auto';
        img.style.msUserSelect = 'auto';
        img.style.webkitTouchCallout = 'default'; // 尝试恢复iOS长按菜单

        // 移除所有可能阻止右键的事件处理器
        img.oncontextmenu = null;
        img.ondragstart = null;
        img.onselectstart = null;
        // 保留 mousedown 和 mouseup 以免影响其他功能,但确保右键可以穿透
        // img.onmousedown = null;
        // img.onmouseup = null;

        // 移除事件监听器(如果存在)
        const events = ['contextmenu', 'dragstart', 'selectstart' /*, 'mousedown', 'mouseup'*/]; // 谨慎移除 mousedown/up
        events.forEach(eventType => {
            img.removeEventListener(eventType, preventDefault, true); // 移除捕获阶段
            img.removeEventListener(eventType, preventDefault, false); // 移除冒泡阶段
        });

        // 添加强制允许右键的事件监听器
        // 使用捕获阶段确保优先处理
        img.addEventListener('contextmenu', function(e) {
            e.stopPropagation(); // 阻止事件冒泡到父元素,以防父元素有阻止右键的逻辑
            console.log('图片右键菜单已启用 (脚本拦截)');
            return true; // 明确允许默认行为
        }, true); // 使用捕获阶段

        img.addEventListener('mousedown', function(e) {
            if (e.button === 2) { // 右键
                e.stopPropagation(); // 阻止事件冒泡
                console.log('图片右键点击已启用 (脚本拦截)');
                // return true; // mousedown 通常不需要 return true 来允许 contextmenu
            }
        }, true); // 使用捕获阶段
    }

    // 阻止事件的通用函数 (如果需要)
    function preventDefault(e) {
        e.preventDefault();
        e.stopPropagation();
        // return false; // 在事件监听器中 return false 等同于 preventDefault + stopPropagation
    }

    // 精准的事件拦截
    function blockZoomEvents() {
        if (zoomEventBlocked) return;
        zoomEventBlocked = true;

        // 拦截可能的缩放函数
        if (window.ImageZoom) {
            window.ImageZoom = function() { console.log('ImageZoom 被禁用 (脚本重写)'); return {}; }; // 返回一个对象模拟
        }
        // 尝试覆盖可能的jQuery插件
        if (typeof jQuery !== 'undefined' && jQuery.fn.imagezoom) {
            jQuery.fn.imagezoom = function() { console.log('jQuery.fn.imagezoom 被禁用'); return this; };
        }


        // 拦截容器上的缩放事件,但保留其他功能
        const originalAddEventListener = Element.prototype.addEventListener;
        Element.prototype.addEventListener = function(type, listener, options) {
            let isMainPicContainerTarget = false;
            if (this.classList) {
                for (const cls of this.classList) {
                    if (cls.includes('--mainPicWrap--')) { // 使用部分匹配
                        isMainPicContainerTarget = true;
                        break;
                    }
                }
            }

            const isZoomMouseEvent = (type === 'mousemove' || type === 'mouseover' || type === 'mouseenter' || type === 'mouseleave' || type === 'mouseout');

            if (isMainPicContainerTarget && isZoomMouseEvent) {
                const listenerStr = listener.toString();
                // 更宽松地匹配可能与缩放相关的事件处理器
                if (listenerStr.includes('zoom') || listenerStr.includes('scale') || listenerStr.includes('lens') || listenerStr.includes('magnif')) {
                    console.log(`拦截了容器 [${Array.from(this.classList).join(', ')}] 的缩放事件: ${type}`);
                    return; // 阻止添加此事件监听器
                }
            }

            // 对于1688 (保持原有逻辑)
            const is1688ZoomElement = this.classList && (this.classList.contains('scale-img') || this.classList.contains('scaled-img'));
            if (is1688ZoomElement) {
                console.log(`拦截了1688缩放元素 [${Array.from(this.classList).join(', ')}] 的事件: ${type}`);
                return;
            }

            return originalAddEventListener.call(this, type, listener, options);
        };
    }

    // 添加CSS样式
    function addZoomDisableStyles() {
        const style = document.createElement('style');
        style.id = 'zoom-disable-styles';

        let cssText = '';

        if (is1688) {
            cssText = `
                /* 1688 缩放元素禁用 */
                .detail-gallery-preview .scale-img,
                .detail-gallery-preview .scaled-img {
                    display: none !important;
                    visibility: hidden !important;
                    opacity: 0 !important;
                    pointer-events: none !important;
                }

                /* 确保1688图片可以右键 */
                .detail-gallery-preview .preview-img {
                    pointer-events: auto !important;
                    user-select: auto !important;
                    -webkit-user-select: auto !important;
                    position: relative !important; /* 确保 z-index 生效 */
                    z-index: 998 !important; /* 比下面的主图低一级,但高于干扰元素 */
                }
            `;
        } else if (isTaobaoTmallCommon) { // 使用合并后的检测
            cssText = `
                /* 淘宝天猫缩放元素禁用 */
                .js-image-zoom__zoomed-area,
                .js-image-zoom__zoomed-image,
                #lensDiv {
                    display: none !important;
                    visibility: hidden !important;
                    opacity: 0 !important;
                    pointer-events: none !important;
                }

                /* 关键修复:让容器鼠标事件穿透,图片获得最高优先级 */
                /* 使用属性选择器 [class*="--mainPicWrap--"] */
                [class*="--mainPicWrap--"] {
                    pointer-events: none !important; /* 容器不接收鼠标事件 */
                }

                /* 让主图片重新获得鼠标事件和最高层级 */
                /* 使用属性选择器 [class*="--mainPic--"] */
                [class*="--mainPic--"]:not(video) { /* 确保这个规则不会意外应用到 video 标签上,如果 class 命名冲突 */
                    pointer-events: auto !important; /* 图片接收鼠标事件 */
                    user-select: auto !important;
                    -webkit-user-select: auto !important;
                    -moz-user-select: auto !important;
                    -ms-user-select: auto !important;
                    position: relative !important; /* 确保 z-index 生效 */
                    z-index: 999 !important;      /* 提升图片层级 */
                    -webkit-touch-callout: default !important;
                    -khtml-user-select: auto !important; /* 针对旧版KHTML */
                }

                /* --- 新增/修改的CSS规则 开始 --- */
                /* 确保视频播放器容器及其所有子元素(包括控制条)可以响应鼠标事件 */
                /* 这个规则会覆盖父级 [class*="--mainPicWrap--"] 设置的 pointer-events: none !important; */
                [class*="--mainPicWrap--"] .videox-container,
                [class*="--mainPicWrap--"] .videox-container * {
                    pointer-events: auto !important;
                    /* 设置一个较高的 z-index 以确保视频控件在主图片之上(如果它们重叠) */
                    z-index: 1000 !important;
                }

                /* 特别为 videox-progress-container 及其子元素强调,确保它们可交互 */
                /* 尽管上面的 videox-container * 应该已经覆盖了这里,但明确指定可以增加可靠性,并允许单独设置更高的 z-index */
                [class*="--mainPicWrap--"] .videox-progress-container,
                [class*="--mainPicWrap--"] .videox-progress-container * {
                    pointer-events: auto !important;
                    z-index: 1001 !important; /* 比其他视频控件更高,确保进度条最优先响应 */
                }
                /* --- 新增/修改的CSS规则 结束 --- */

                /* 确保缩略图可以点击 */
                [class*="--thumbnail--"],
                [class*="--thumbnailPic--"] {
                    pointer-events: auto !important;
                    z-index: 99 !important; /* 确保缩略图在某些情况下可点 */
                }

                /* 确保切换标签可以点击 */
                [class*="--switchTabsItem--"] {
                    pointer-events: auto !important;
                    z-index: 99 !important;
                }

                /* 确保缩略图容器可以交互 */
                [class*="--thumbnailsWrap--"],
                [class*="--thumbnails--"] { /* 通常 thumbnails 是 wrap 内部的列表 */
                    pointer-events: auto !important;
                }

                /* 确保底部切换标签容器可以交互 */
                [class*="--bottomSwitchTabsWrap--"],
                [class*="--switchTabsWrap--"] {
                    pointer-events: auto !important;
                }
            `;
        }

        style.textContent = cssText;

        if (!document.getElementById('zoom-disable-styles')) {
            (document.head || document.documentElement).appendChild(style);
        } else {
            // 如果样式已存在,则更新其内容
            document.getElementById('zoom-disable-styles').textContent = cssText;
        }
    }

    // 强制移除右键阻止
    function forceEnableRightClickOnDocument() {
        // 移除可能的全局右键阻止
        document.oncontextmenu = null;
        document.onselectstart = null;
        document.ondragstart = null;

        // 移除body上的右键阻止
        if (document.body) {
            document.body.oncontextmenu = null;
            document.body.onselectstart = null;
            document.body.ondragstart = null;
        }
        // 尝试移除document上的事件监听
        const eventsToClear = ['contextmenu', 'selectstart', 'dragstart'];
        eventsToClear.forEach(eventType => {
            // Note: This won't remove listeners added with addEventListener directly without a reference.
            // This is more of a "best effort" for listeners assigned via on<eventtype>.
            // For addEventListener, specific targeting or overriding prototypes is needed (which is partially done in blockZoomEvents)
        });
    }

    // 监控动态变化
    function setupObserver() {
        const observer = new MutationObserver(function(mutations) {
            let needsReapply = false;
            mutations.forEach(function(mutation) {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    mutation.addedNodes.forEach(function(node) {
                        if (node.nodeType === 1) { // Element node
                            // 检查是否是主图容器或主图本身,或者其他相关元素
                            if ( (node.matches && (node.matches('[class*="--mainPicWrap--"]') || node.matches('[class*="--mainPic--"]'))) ||
                                 (node.querySelector && (node.querySelector('[class*="--mainPicWrap--"]') || node.querySelector('[class*="--mainPic--"]'))) ) {
                                needsReapply = true;
                            }
                            // 针对1688
                            if (is1688 && ((node.matches && (node.matches('.scale-img') || node.matches('.scaled-img'))) ||
                                (node.querySelector && (node.querySelector('.scale-img') || node.querySelector('.scaled-img')))) ) {
                                needsReapply = true;
                            }
                            // 针对通用缩放层
                            if ( (node.matches && (node.matches('.js-image-zoom__zoomed-area') || node.matches('.js-image-zoom__zoomed-image') || node.id === 'lensDiv')) ||
                                 (node.querySelector && (node.querySelector('.js-image-zoom__zoomed-area') || node.querySelector('.js-image-zoom__zoomed-image') || node.querySelector('#lensDiv'))) ) {
                                needsReapply = true;
                            }
                        }
                    });
                } else if (mutation.type === 'attributes') {
                    // 如果某些关键元素的 class 或 style 变化,也可能需要重新应用
                    if (mutation.target.matches && (mutation.target.matches('[class*="--mainPicWrap--"]') || mutation.target.matches('[class*="--mainPic--"]'))) {
                        needsReapply = true;
                    }
                }
            });

            if (needsReapply) {
                console.log('DOM变化,重新应用缩放禁用和右键修复');
                disableImageZoom(); // This will call either handle1688Zoom or handleTaobaoTmallZoom
                // forceEnableRightClickOnDocument(); // 通常 enableImageRightClick 作用于特定图片元素已经足够
            }
        });

        observer.observe(document.documentElement || document.body, { // Observe the whole document if body isn't ready
            childList: true,
            subtree: true,
            attributes: true, // 监听属性变化,以防样式被动态修改
            attributeFilter: ['class', 'style'] // 只关心class和style属性
        });
        console.log('MutationObserver已设置');
    }


    // 初始化函数
    function init() {
        console.log('初始化电商图片缩放禁用脚本 (修复版)...');

        blockZoomEvents(); // 应该尽早执行,在页面脚本运行前尝试Hook
        addZoomDisableStyles(); // 注入基础CSS
        disableImageZoom(); // 首次尝试处理
        forceEnableRightClickOnDocument(); // 尝试清除全局右键阻止

        // DOMContentLoaded 后再次确保,因为 blockZoomEvents 修改了原型,可能需要页面元素存在
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                console.log('DOMContentLoaded: 应用修复');
                addZoomDisableStyles(); // 确保样式在DOM加载后应用
                disableImageZoom();
                forceEnableRightClickOnDocument();
                setupObserver(); // 在DOM加载完毕后启动观察者
            });
        } else {
            // 如果 'loading' 阶段已过 (interactive or complete)
            console.log('Document already loaded: 应用修复并设置观察者');
            addZoomDisableStyles();
            disableImageZoom();
            forceEnableRightClickOnDocument();
            setupObserver();
        }


        // 延迟执行,应对某些异步加载的组件
        setTimeout(() => {
            console.log('延迟修复 (1s)');
            addZoomDisableStyles(); // 确保样式
            disableImageZoom();
            forceEnableRightClickOnDocument();
        }, 1000);

        setTimeout(() => {
            console.log('二次延迟修复 (3s)');
            addZoomDisableStyles(); // 确保样式
            disableImageZoom();
            forceEnableRightClickOnDocument();
        }, 3000);
    }

    // @run-at document-start 意味着脚本会很早执行
    // 因此,init() 中的 DOMContentLoaded 监听器和直接执行路径都很重要

    init(); // 调用初始化

    // 定期维护 (作为最后的保障,但 MutationObserver 应该是主要手段)
    const maintenanceInterval = setInterval(() => {
        // console.log('定期维护检查');
        disableImageZoom();
        // forceEnableRightClickOnDocument(); // 这个可能过于频繁,主要依赖 enableImageRightClick
    }, 5000);

    // 页面卸载时清理
    window.addEventListener('unload', () => {
        if (maintenanceInterval) {
            clearInterval(maintenanceInterval);
        }
        // 理论上 MutationObserver 会自动停止,但也可以显式disconnect
    });

    console.log('电商图片缩放禁用脚本加载完成 (修复版) - 专门修复右键问题');
})();