您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Enhance your AliExpress shopping experience by converting marketing links into direct product links, ensuring each product is easily accessible with a single click.
// ==UserScript== // @name AliExpress Product Link Fixer // @namespace http://tampermonkey.net/ // @version 2.3 // @license MIT // @description Enhance your AliExpress shopping experience by converting marketing links into direct product links, ensuring each product is easily accessible with a single click. // @author NewsGuyTor // @match https://*.aliexpress.com/* // @icon https://www.aliexpress.com/favicon.ico // @grant GM_openInTab // ==/UserScript== (function() { 'use strict'; // --- Global Variables --- let observer; // MutationObserver instance to watch for page changes let debounceTimer; // Timer ID for debouncing MutationObserver callbacks let isHandlingClick = false; // Flag to prevent click handler re-entry issues // --- Core Functions --- /** * Schedules the main link fixing logic to run after a short delay. * This prevents the function from running excessively on rapid DOM changes. */ function scheduleFixLinks() { clearTimeout(debounceTimer); debounceTimer = setTimeout(fixLinks, 250); // Wait 250ms after the last mutation } /** * Main function orchestrating the different link fixing strategies. * Temporarily disconnects the observer to avoid infinite loops while modifying the DOM. */ function fixLinks() { if (observer) observer.disconnect(); // Pause observation during modification try { // Apply fixes in a specific order removeMarketingAnchors(); // Phase 1: Clean up wrapper links without direct product IDs (carefully) rewriteAnchorsWithProductIds(); // Phase 2: Correct links that *do* contain product IDs fixOrCreateLinksForDataProducts(); // Phase 3: Ensure product elements (divs/etc.) are properly linked } catch (err) { console.error("[AliExpress Product Link Fixer v2.3] Error in fixLinks():", err); } // Resume observation after modifications are done if (observer) { try { observer.observe(document.body, { childList: true, subtree: true }); } catch (e) { // Handle edge case where the page unloads rapidly if (e.name !== 'NotFoundError') console.error("[AliFix v2.3] Error reconnecting observer:", e); } } else { console.warn("[AliFix v2.3] Observer not ready."); } } /** * Phase 1: Removes wrapper anchor tags (`<a>`) that point to marketing URLs * (containing '/gcp/' or '/ssr/') *without* a specific 'productIds' parameter. * It unwraps the anchor, keeping its child elements in place. * It avoids unwrapping anchors that appear to be structural components (containing complex nested divs) * or specific excluded navigation elements. */ function removeMarketingAnchors() { // Select potential marketing anchors not yet processed by this script const anchors = document.querySelectorAll('a[href*="/gcp/"]:not([data-alifix-done]), a[href*="/ssr/"]:not([data-alifix-done])'); anchors.forEach(a => { // Skip if already marked as processed if (a.dataset.alifixDone) return; // --- Exclusion Checks --- // 1. Exclude specific header navigation links (e.g., Bundle Deals, Choice tabs) const headerNavContainer = a.closest('div.an_ar.an_at[data-tabs="true"]'); if (headerNavContainer) { a.dataset.alifixDone = "1"; // Mark as processed to prevent other functions touching it return; // Do not unwrap header links } // --- Process other potential marketing links --- try { if (!a.href) { if (a && a.dataset) a.dataset.alifixDone = "1"; return; } // Skip anchors without href const url = new URL(a.href, location.origin); // Only consider anchors without productIds for unwrapping if (!url.searchParams.has('productIds')) { // --- Structural Check: Avoid unwrapping complex/structural anchors --- let hasDirectDivChild = false; for (let i = 0; i < a.children.length; i++) { if (a.children[i].tagName === 'DIV') { hasDirectDivChild = true; break; } } if (hasDirectDivChild) { // This anchor contains a direct DIV child. It might be a structural element. Let's NOT unwrap it. if (a && a.dataset) a.dataset.alifixDone = "1"; return; // Skip unwrapping } // --- End of Structural Check --- // If we pass the checks, proceed to unwrap if (a && a.dataset) a.dataset.alifixDone = "1"; // Mark before unwrapping unwrapAnchor(a); // Remove the anchor, keep children } // Anchors with productIds are handled by rewriteAnchorsWithProductIds. } catch (e) { console.error("[AliFix v2.3] Error processing anchor for potential removal:", a.href, e); if (a && a.dataset) a.dataset.alifixDone = "1"; // Mark as done on error } }); } /** * Phase 2: Rewrites anchor tags that point to marketing URLs ('/gcp/' or '/ssr/') * but *do* contain a 'productIds' parameter. It changes the href to point * directly to the standard '/item/...' product page URL. */ function rewriteAnchorsWithProductIds() { // Select relevant anchors not yet processed const anchors = document.querySelectorAll('a[href*="/gcp/"]:not([data-alifix-done]), a[href*="/ssr/"]:not([data-alifix-done])'); anchors.forEach(a => { if (a.dataset.alifixDone) return; // Skip already processed try { if (!a.href) { if (a && a.dataset) a.dataset.alifixDone = "1"; return; } const url = new URL(a.href, location.origin); const pidParam = url.searchParams.get('productIds'); if (pidParam) { const actualPid = pidParam.split(':')[0]; if (actualPid && /^\d+$/.test(actualPid)) { const newHref = `https://${url.host}/item/${actualPid}.html`; if (a.href !== newHref) { a.href = newHref; } if (a.dataset) a.dataset.alifixDone = "1"; // Mark as successfully processed } else { console.warn("[AliFix v2.3] Invalid PID format found:", pidParam, "in anchor:", a.href); if (a.dataset) a.dataset.alifixDone = "1"; // Mark as processed even if PID format was invalid } } else { // Mark anchors without productIds as done here if they weren't unwrapped in phase 1 if (a.dataset) a.dataset.alifixDone = "1"; } } catch (e) { console.error("[AliFix v2.3] Error processing anchor for rewrite:", a.href, e); if (a && a.dataset) a.dataset.alifixDone = "1"; // Mark done on error } }); } /** * Phase 3: Ensures that elements representing products have a functional, direct link. * Targets elements identified either by 'data-product-ids' attribute or specific * div structures (like those on Bundle Deals pages with numeric IDs, identified more robustly). * If a correct link doesn't exist, it creates a wrapper `<a>` tag. * Applies CSS `pointer-events: none` to the original inner element when wrapping, * to prevent interference from its original JS click handlers. * Attaches a custom click handler to newly created links to manage navigation reliably. */ function fixOrCreateLinksForDataProducts() { // Select potential product elements using various known patterns, excluding already processed ones const productIndicators = document.querySelectorAll( '[data-product-ids]:not([data-alifix-done]), ' + // Common pattern '.g6_cy[data-product-ids]:not([data-alifix-done]), ' + // Specific class often used with data-product-ids // --- Robust Selectors for Bundle Deals Containers --- '#root div[mod-name*="waterfall"] div[id][class*="productContainer"]:not([data-alifix-done]), ' + // Waterfall view '#root div[mod-name*="goods-slider"] div[id][class*="productContainer"]:not([data-alifix-done]), ' + // Slider view // --- End Robust Selectors --- 'div.jc_bt.jc_jl[data-product-ids]:not([data-alifix-done])' // AliExpress Business section pattern ); productIndicators.forEach(element => { let pid; let isBundleDealsDiv = false; // Flag if it's a Bundle Deals div needing wrapping if (!element || !element.dataset || element.dataset.alifixDone) return; // Determine the source of the Product ID if (element.dataset.productIds) { pid = element.dataset.productIds.split(':')[0]; // Handle potential extra chars } else if (element.tagName === 'DIV' && element.id && /^\d+$/.test(element.id)) { // Check if it matches our robust selectors for Bundle Deals divs if (element.matches('#root div[mod-name*="waterfall"] div[id][class*="productContainer"], #root div[mod-name*="goods-slider"] div[id][class*="productContainer"]')) { pid = element.id; isBundleDealsDiv = true; // Mark it for wrapping } else { // It's a div with a numeric ID, but doesn't match the Bundle Deals patterns we target if (element.dataset) element.dataset.alifixDone = "1"; return; } } else { if (element.dataset) element.dataset.alifixDone = "1"; return; } // No valid PID source found // Validate the extracted/found Product ID if (!pid || !/^\d+$/.test(pid)) { console.warn("[AliFix v2.3] Invalid PID found for element:", pid, element); if (element.dataset) element.dataset.alifixDone = "1"; return; } // Mark the element as processed EARLY to prevent potential infinite loops element.dataset.alifixDone = "1"; const targetHref = `https://${location.host}/item/${pid}.html`; // --- Check if linking is already handled --- // 1. Check if correctly wrapped by a *direct* parent anchor const parentAnchor = element.parentNode; if (parentAnchor && parentAnchor.tagName === 'A') { if (parentAnchor.href === targetHref) { if (!parentAnchor.dataset.alifixDone) parentAnchor.dataset.alifixDone = "1"; return; // Already handled } else if (!parentAnchor.dataset.alifixDone) { parentAnchor.href = targetHref; parentAnchor.dataset.alifixDone = "1"; return; // Handled } else { return; } // Already marked done } // 2. For elements identified by `data-product-ids`, check for an *inner* anchor to fix. // Skip this check if the element itself is an anchor OR if it's a BundleDealsDiv (which we intend to wrap). if (!isBundleDealsDiv && element.tagName !== 'A') { const existingInnerAnchor = element.querySelector('a:not([data-alifix-link-added]):not([id*="/^\\d+$/"])'); if (existingInnerAnchor && !existingInnerAnchor.dataset.alifixDone) { if (existingInnerAnchor.href !== targetHref) { existingInnerAnchor.href = targetHref; } existingInnerAnchor.dataset.alifixDone = "1"; return; // Handled } } // --- Create a new wrapper link if no suitable parent/inner link was found/fixed --- if (!element.parentNode) { console.warn(`[AliFix v2.3] Element for PID ${pid} lost its parent before wrapping.`); return; } const link = document.createElement('a'); link.href = targetHref; link.dataset.alifixDone = "1"; link.dataset.alifixLinkAdded = "1"; link.style.display = 'block'; link.style.color = 'inherit'; link.style.textDecoration = 'none'; link.style.cursor = 'pointer'; link.addEventListener('click', handleProductClick, true); try { element.parentNode.insertBefore(link, element); link.appendChild(element); // Disable pointer events on the original div to override AE's JS listeners element.style.setProperty('pointer-events', 'none', 'important'); } catch (e) { console.error(`[AliFix v2.3] Error wrapping element PID ${pid}:`, e, element); } }); } /** * Custom click handler attached ONLY to newly created wrapper anchors. * Prevents the anchor's default navigation and stops the event from propagating. * Handles opening in new tab (Ctrl/Cmd/Middle-click) or same tab manually. * Uses a guard flag (`isHandlingClick`) to prevent issues from rapid/double clicks. */ function handleProductClick(event) { // Prevent re-entry if handler is already running if (isHandlingClick) { event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); return false; } isHandlingClick = true; // Immediately stop the default action (navigation) and prevent event propagation event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); // Stop other listeners on this same element too const link = event.currentTarget; // The anchor element we attached the listener to const href = link.href; // Double-check the link is valid before navigating if (!href || !href.startsWith('http') || !href.includes('/item/')) { console.warn("[AliFix v2.3] Click handler stopped non-navigable/invalid link:", href, "Target:", event.target); setTimeout(() => { isHandlingClick = false; }, 50); return false; // Ensure no fallback default action occurs } // Determine if a new tab is requested (Middle mouse, Ctrl+click, Cmd+click) const isMiddleClick = event.button === 1; const isCtrlClick = event.ctrlKey; const isMetaClick = event.metaKey; // Cmd on Mac const openInNewTab = isMiddleClick || isCtrlClick || isMetaClick; // Manually perform the navigation if (openInNewTab) { if (typeof GM_openInTab === 'function') { try { GM_openInTab(href, { active: isMiddleClick, insert: true }); } catch (gmErr) { console.error("[AliFix v2.3] Error using GM_openInTab:", gmErr, "Falling back to window.open."); window.open(href, '_blank'); // Fallback on GM error } } else { console.warn("[AliFix v2.3] GM_openInTab not available/granted, using window.open."); window.open(href, '_blank'); // Fallback if GM function doesn't exist } } else if (event.button === 0) { // Standard left click window.location.href = href; // Navigate in the current tab } // Reset the re-entry guard after a short delay setTimeout(() => { isHandlingClick = false; }, 50); return false; // Standard practice to return false from handlers that prevent default } /** * Helper function to remove a wrapper element (typically an anchor) * while keeping its child nodes in the same position in the DOM. * @param {HTMLElement} wrapper - The element to remove. */ function unwrapAnchor(wrapper) { const parent = wrapper.parentNode; if (!parent || !wrapper) return; // Safety check try { while (wrapper.firstChild) { parent.insertBefore(wrapper.firstChild, wrapper); } if (wrapper.parentNode === parent) { parent.removeChild(wrapper); } } catch (e) { console.error("[AliFix v2.3] Error unwrapping element:", wrapper, e); } } // --- Initialization and Observation --- // Run the fixes once initially after the DOM is ready or loaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', fixLinks); } else { // DOM already loaded, run after a short delay to allow page JS to potentially settle setTimeout(fixLinks, 150); } // Create and start the MutationObserver to watch for dynamically loaded content observer = new MutationObserver(scheduleFixLinks); // Use the debounced scheduler // Observe the body initially. If body isn't available yet, wait. function startObserver() { if (document.body) { observer.observe(document.body, { childList: true, // Watch for addition/removal of nodes subtree: true // Watch descendants as well }); console.log("[AliExpress Product Link Fixer v2.3] Initialized and observing."); } else { console.warn("[AliFix v2.3] Document body not ready, retrying observer start..."); setTimeout(startObserver, 100); } } startObserver(); })();