您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add price per unit and discount percentage to products and promotion cards (updated for new page structures)
// ==UserScript== // @name Albert Heijn Korting Updated (with Promotion Parsing) // @namespace https://wol.ph/ // @version 1.0.6 // @description Add price per unit and discount percentage to products and promotion cards (updated for new page structures) // @author wolph // @match https://www.ah.nl/* // @icon https://icons.duckduckgo.com/ip2/ah.nl.ico // @grant none // @license BSD // ==/UserScript== const DEBUG = false; (function () { 'use strict'; // Set to keep track of processed products and cards to avoid duplicate processing. const processedProducts = new Set(); function updateProductInfo() { console.log("Starting updateProductInfo"); // --- PROCESS PRODUCT CARDS --- let productCards = document.querySelectorAll('article[data-testhook="product-card"]'); console.log(`Found ${productCards.length} product cards`); productCards.forEach(function (productCard) { // Use a unique identifier for the product let productId = productCard.getAttribute("data-product-id") || productCard.querySelector('a[class*="link_root__"]')?.getAttribute("href"); if (!productId) { console.log("Product ID not found, skipping product-card"); return; } if (processedProducts.has(productId)) { return; } console.log(`Processing product ${productId}`); // --- Extract current price --- // Look for the highlighted price element let priceElement = productCard.querySelector('[data-testhook="price-amount"].price-amount_highlight__ekL92'); if (!priceElement) { console.log("Highlighted price element not found, skipping product-card"); return; } // Extract integer and fractional parts let priceInt = priceElement.querySelector('[class*="price-amount_integer__"]'); let priceFrac = priceElement.querySelector('[class*="price-amount_fractional__"]'); if (!priceInt || !priceFrac) { console.log("Price integer or fractional part not found, skipping product-card"); return; } let price = parseFloat(priceInt.textContent + "." + priceFrac.textContent); console.log(`Price: ${price}`); // --- Extract unit size --- let unitSizeElement = productCard.querySelector('[data-testhook="product-unit-size"]'); if (!unitSizeElement) { console.log("Unit size element not found, skipping product-card"); return; } let unitSizeText = unitSizeElement.textContent.trim(); // e.g., "ca. 755 g" or "750 g" console.log(`Unit size text: ${unitSizeText}`); // Parse unit size (supports weight and count). let unitMatch = unitSizeText.match(/(ca\.?\s*)?([\d.,]+)\s*(g|kg)/i); let countMatch = unitSizeText.match(/(\d+)\s*(stuk|stuks)/i); if (!unitMatch && !countMatch) { console.log("Weight and count not found in unit size text, skipping product-card"); return; } let pricePerUnit = 0; let unit = ""; if (unitMatch) { let weight = parseFloat(unitMatch[2].replace(",", ".")); unit = unitMatch[3].toLowerCase(); console.log(`Weight: ${weight}, Unit: ${unit}`); // Convert weight to grams if needed; we calculate per kg if (unit === "kg") { weight = weight * 1000; console.log(`Converted weight to grams: ${weight}`); } else { // We display per kg even if provided in grams. unit = "kg"; } // Calculate price per kg pricePerUnit = (price * 1000) / weight; } if (countMatch) { let count = parseInt(countMatch[1]); unit = countMatch[2].toLowerCase(); console.log(`Count: ${count}`); // Calculate price per item pricePerUnit = price / count; } console.log(`Price per ${unit}: €${pricePerUnit.toFixed(2)}`); // --- Extract old price, if available --- let oldPrice = null; let oldPriceElement = productCard.querySelector('[data-testhook="price-amount"]:not(.price-amount_highlight__ekL92)'); if (oldPriceElement) { let oldPriceInt = oldPriceElement.querySelector('[class*="price-amount_integer__"]'); let oldPriceFrac = oldPriceElement.querySelector('[class*="price-amount_fractional__"]'); if (oldPriceInt && oldPriceFrac) { oldPrice = parseFloat(oldPriceInt.textContent + "." + oldPriceFrac.textContent); console.log(`Old price: ${oldPrice}`); } } // --- Calculate discount percentage --- let discountPercentage = null; if (oldPrice && oldPrice > price) { discountPercentage = (((oldPrice - price) / oldPrice) * 100).toFixed(1); console.log(`Calculated discount from prices: ${discountPercentage}%`); } else { // Try extracting discount percentage from shield promotion text let shieldElement = productCard.querySelector('[data-testhook="product-shield"]'); if (shieldElement) { let shieldTextElement = shieldElement.querySelector('[class*="shield_text__"]'); if (shieldTextElement) { let shieldText = shieldTextElement.textContent.trim(); discountPercentage = parsePromotionText(shieldText, price) || "0"; console.log(`Extracted discount percentage from shield: ${discountPercentage}%`); } else { discountPercentage = "0"; console.log("No shield text found, setting discount to 0%"); } } else { discountPercentage = "0"; console.log("Shield element not found, setting discount to 0%"); } } // --- Create or update shield element --- let shieldElement = productCard.querySelector('[data-testhook="product-shield"]'); if (!shieldElement) { console.log("Creating shield element for product-card"); shieldElement = document.createElement("div"); shieldElement.className = "shield_root__SmhpN"; shieldElement.setAttribute("data-testhook", "product-shield"); let newShieldTextElement = document.createElement("span"); newShieldTextElement.className = "shield_text__kNeiW"; shieldElement.appendChild(newShieldTextElement); let shieldContainer = productCard.querySelector('[class*="product-card-portrait_shieldProperties__"]'); if (!shieldContainer) { console.log("Creating shield container for product-card"); shieldContainer = document.createElement("div"); shieldContainer.className = "product-card-portrait_shieldProperties__+JZJI"; let header = productCard.querySelector('[class*="header_root__"]'); if (header === null) return; header.appendChild(shieldContainer); } shieldContainer.appendChild(shieldElement); } let shieldTextElement = shieldElement.querySelector('[class*="shield_text__"]'); if (!shieldTextElement) { shieldTextElement = document.createElement("span"); shieldTextElement.className = "shield_text__kNeiW"; shieldElement.appendChild(shieldTextElement); } // Update the shield text with discount percentage shieldTextElement.textContent = `${discountPercentage}%`; // Set background and text color based on discount percentage let { backgroundColor, textColor } = getDiscountColors(discountPercentage); shieldElement.style.backgroundColor = backgroundColor; shieldElement.style.color = textColor; // --- Update price element to include price per unit info --- let priceContainer = priceElement.parentElement; if (priceContainer) { let pricePerUnitElement = priceContainer.querySelector(".price-per-unit"); if (!pricePerUnitElement) { pricePerUnitElement = document.createElement("div"); pricePerUnitElement.className = "price-per-unit"; pricePerUnitElement.style.fontSize = "smaller"; priceContainer.appendChild(pricePerUnitElement); } pricePerUnitElement.textContent = `€${pricePerUnit.toFixed(2)} per ${unit}`; } // Mark this product as processed processedProducts.add(productId); console.log(`Product ${productId} processed`); }); // --- PROCESS PROMOTION CARDS (NEW STRUCTURE) --- let promotionCards = document.querySelectorAll('a.promotion-card_root__tQA3z'); console.log(`Found ${promotionCards.length} promotion cards`); promotionCards.forEach(function (card) { // Use the element's id or href as a unique identifier let cardId = card.getAttribute("id") || card.getAttribute("href"); if (!cardId) { console.log("Promotion card unique id not found, skipping"); return; } if (processedProducts.has(cardId)) { return; } console.log(`Processing promotion card ${cardId}`); // --- Extract current price --- let priceElem = card.querySelector('[data-testhook="price"]'); if (!priceElem) { console.log("Promotion price element not found, skipping promotion card"); return; } let priceNow = priceElem.getAttribute("data-testpricenow"); if (!priceNow) { console.log("Promotion current price attribute missing, skipping"); return; } let currentPrice = parseFloat(priceNow); console.log(`Promotion current price: ${currentPrice}`); // --- Extract old price, if available --- let oldPriceAttr = priceElem.getAttribute("data-testpricewas"); let oldPrice = oldPriceAttr ? parseFloat(oldPriceAttr) : null; if (oldPrice) { console.log(`Promotion old price: ${oldPrice}`); } // --- Parse unit size from title --- // We assume that the card title contains unit information (e.g. "400 g", "1 l", "2 stuks") let titleElem = card.querySelector('[data-testhook="card-title"]'); if (!titleElem) { console.log("Promotion card title not found, skipping unit extraction"); return; } let titleText = titleElem.textContent.trim(); console.log(`Promotion card title: "${titleText}"`); let unitMatch = titleText.match(/(ca\.?\s*)?([\d.,]+)\s*(g|kg)/i); let countMatch = titleText.match(/(\d+)\s*(stuk|stuks)/i); let ppu = 0; let unit = ""; if (unitMatch) { let weight = parseFloat(unitMatch[2].replace(",", ".")); unit = unitMatch[3].toLowerCase(); console.log(`Parsed weight: ${weight} ${unit}`); if (unit === "kg") { weight *= 1000; } else { unit = "kg"; // we display per kg even if provided in grams } ppu = (currentPrice * 1000) / weight; } else if (countMatch) { let count = parseInt(countMatch[1]); unit = "stuk"; console.log(`Parsed count: ${count} ${unit}(s)`); ppu = currentPrice / count; } else { console.log("No unit size found in promotion card title, skipping price per unit calculation"); } // --- Calculate discount percentage --- let discountPercentage = null; if (oldPrice && oldPrice > currentPrice) { discountPercentage = (((oldPrice - currentPrice) / oldPrice) * 100).toFixed(1); console.log(`Calculated discount from promotion prices: ${discountPercentage}%`); } else { discountPercentage = "0"; console.log("No discount available on promotion card"); } // --- Update shield element --- // Promotion cards usually include a shield container with promotion texts. let shieldElem = card.querySelector('[data-testhook="promotion-shields"]'); if (shieldElem) { // Look for an element that displays promotion text within shield. let shieldTextElem = shieldElem.querySelector('[data-testhook="promotion-text"]'); if (shieldTextElem) { shieldTextElem.textContent = discountPercentage + "%"; } else { shieldTextElem = document.createElement("span"); shieldTextElem.setAttribute("data-testhook", "promotion-text"); shieldTextElem.textContent = discountPercentage + "%"; shieldElem.appendChild(shieldTextElem); } let colors = getDiscountColors(discountPercentage); shieldElem.style.backgroundColor = colors.backgroundColor; shieldElem.style.color = colors.textColor; } // --- Update or create price-per-unit element --- let priceContainer = priceElem.parentElement; if (priceContainer) { let ppuElem = priceContainer.querySelector(".price-per-unit"); if (!ppuElem) { ppuElem = document.createElement("div"); ppuElem.className = "price-per-unit"; ppuElem.style.fontSize = "smaller"; priceContainer.appendChild(ppuElem); } if (ppu > 0) { ppuElem.textContent = "€" + ppu.toFixed(2) + " per " + unit; } } processedProducts.add(cardId); console.log(`Promotion card ${cardId} processed`); }); console.log("Finished updateProductInfo"); } // Helper function to parse promotion text (e.g., "1 + 1 gratis") function parsePromotionText(shieldText, pricePerItem) { shieldText = shieldText.toLowerCase(); let discountPercentage = null; if (shieldText.includes("%")) { let discountMatch = shieldText.match(/(\d+)%\s*korting/i); if (discountMatch) { discountPercentage = parseFloat(discountMatch[1]); } } else if (shieldText.includes("gratis")) { if (shieldText.includes("1+1") || shieldText.includes("1 + 1")) { discountPercentage = 50; } else if (shieldText.includes("2+1") || shieldText.includes("2 + 1")) { discountPercentage = 33.33; } } else if (shieldText.includes("2e halve prijs")) { discountPercentage = 25; } return discountPercentage; } function getDiscountColors(discountPercentage) { let backgroundColor, textColor; discountPercentage = parseFloat(discountPercentage); if (discountPercentage >= 80) { backgroundColor = "#008000"; // Dark Green textColor = "#FFFFFF"; // White } else if (discountPercentage >= 60) { backgroundColor = "#32CD32"; // Lime Green textColor = "#000000"; // Black } else if (discountPercentage >= 40) { backgroundColor = "#FFFF00"; // Yellow textColor = "#000000"; // Black } else if (discountPercentage >= 20) { backgroundColor = "#FFA500"; // Orange textColor = "#000000"; // Black } else { backgroundColor = "#FF0000"; // Red textColor = "#FFFFFF"; // White } return { backgroundColor, textColor }; } // Initial run and periodic update window.setTimeout(updateProductInfo, 1000); if (!DEBUG) setInterval(updateProductInfo, 5000); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址