您需要先安装一个扩展,例如 篡改猴、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.2 // @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 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.2] 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.2] Error reconnecting observer:", e); } } else { console.warn("[AliFix v2.2] 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 specifically excludes certain header navigation links. */ 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; // 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; } const url = new URL(a.href, location.origin); // If the URL has '/gcp/' or '/ssr/' but NO 'productIds', unwrap it if (!url.searchParams.has('productIds')) { if (a && a.dataset) a.dataset.alifixDone = "1"; // Mark before unwrapping unwrapAnchor(a); // Remove the anchor, keep children } } catch (e) { console.error("[AliFix v2.2] Error processing anchor for 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; 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) { // Extract the numeric ID (sometimes includes extra chars like ':0') 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; } a.dataset.alifixDone = "1"; // Mark as successfully processed } else { a.dataset.alifixDone = "1"; } // Mark as processed even if PID format was invalid } else { a.dataset.alifixDone = "1"; // Mark anchors without productIds as done here too } } catch (e) { console.error("[AliFix v2.2] 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). * 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 '#root div[id].productContainer:not([data-alifix-done]), ' + // Bundle Deals slider container pattern '#root div[id].product_a12766ed:not([data-alifix-done]), ' + // Bundle Deals waterfall container pattern '#root div[id].product_6a40c3cf:not([data-alifix-done])' // Bundle Deals slider item pattern ); productIndicators.forEach(element => { let pid; let isDivById = false; // Track if PID comes from element ID if (!element || !element.dataset || element.dataset.alifixDone) return; // Determine the source of the Product ID if (element.dataset.productIds) { pid = element.dataset.productIds; } else if (element.tagName === 'DIV' && element.id && /^\d+$/.test(element.id)) { const parentAnchor = element.parentNode; // Only proceed if it looks like it needs wrapping if ((element.getAttribute('href') === "" || !element.hasAttribute('href')) && !(parentAnchor && parentAnchor.tagName === 'A')) { pid = element.id; isDivById = true; } else { element.dataset.alifixDone = "1"; return; } } else { if (element.dataset) element.dataset.alifixDone = "1"; return; } // Validate the extracted/found Product ID if (!pid || !/^\d+$/.test(pid)) { 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 already correctly wrapped by a parent anchor const parentAnchor = element.parentNode; if (parentAnchor && parentAnchor.tagName === 'A') { if (parentAnchor.href === targetHref) { if (!parentAnchor.dataset.alifixDone) parentAnchor.dataset.alifixDone = "1"; return; } else if (!parentAnchor.dataset.alifixDone) { parentAnchor.href = targetHref; parentAnchor.dataset.alifixDone = "1"; return; } else { return; } } // 2. For `data-product-ids` elements, check for an inner anchor to fix if (!isDivById) { const existingInnerAnchor = element.querySelector('a[href]'); if (existingInnerAnchor && !existingInnerAnchor.dataset.alifixDone && !(/^\d+$/.test(existingInnerAnchor.id))) { if (existingInnerAnchor.href !== targetHref) { existingInnerAnchor.href = targetHref; } existingInnerAnchor.dataset.alifixDone = "1"; return; } } // --- Create a new wrapper link if no suitable parent/inner link was found/fixed --- const link = document.createElement('a'); link.href = targetHref; link.dataset.alifixDone = "1"; // Mark the new link itself as processed link.style.display = 'block'; // Ensure proper layout wrapping link.style.color = 'inherit'; // Prevent default blue link color on content link.style.textDecoration = 'none'; // Prevent underline // Attach our custom click handler to manage navigation and prevent JS conflicts link.addEventListener('click', handleProductClick, true); // Use 'click' event, capture phase // Perform the DOM manipulation (wrapping) try { if (element.parentNode) { // Ensure element is still in the DOM element.parentNode.insertBefore(link, element); // Insert link before element link.appendChild(element); // Move element inside link // Disable pointer events on the original element to prevent its JS handlers firing element.style.pointerEvents = 'none'; } else { console.warn(`[AliFix v2.2] Element for PID ${pid} lost its parent before wrapping.`); } } catch (e) { console.error(`[AliFix v2.2] Error wrapping element PID ${pid}:`, e); } }); } /** * Custom click handler attached ONLY to newly created wrapper anchors. * Prevents the anchor's default navigation and stops the event from propagating * to potentially conflicting JS listeners on the original wrapped element. * 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) 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')) { console.warn("[AliFix v2.2] Click handler stopped non-navigable event.", event.target, href); setTimeout(() => { isHandlingClick = false; }, 10); // Reset flag after a short delay 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) { // Use GM_openInTab for better userscript integration if available/granted if (typeof GM_openInTab === 'function') { // Open in background if Ctrl/Cmd was used, otherwise activate (for middle click) GM_openInTab(href, { active: isMiddleClick, insert: true }); } else { console.warn("[AliFix v2.2] GM_openInTab not available/granted, using window.open."); window.open(href, '_blank'); // Fallback } } else if (event.button === 0) { // Standard left click window.location.href = href; // Navigate in the current tab } // Reset the re-entry guard after a very short delay setTimeout(() => { isHandlingClick = false; }, 10); 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 { // Move all children out from wrapper to before the wrapper while (wrapper.firstChild) { parent.insertBefore(wrapper.firstChild, wrapper); } // Remove the now-empty wrapper if it's still attached to the original parent if (wrapper.parentNode === parent) { parent.removeChild(wrapper); } } catch (e) { console.error("[AliFix v2.2] 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, 100); } // Create and start the MutationObserver to watch for dynamically loaded content observer = new MutationObserver(scheduleFixLinks); // Use the debounced scheduler 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.2] Initialized."); })();