// ==UserScript== // @name 店小蜜价格助手 // @namespace http://tampermonkey.net/ // @version 1.6.5 // @description 5倍价格 // @author Rayu // @match https://www.dianxiaomi.com/web/shopeeSite/edit* // @grant none // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; // --- 配置项 --- const HIGHLIGHT_COLOR = 'yellow'; // 高亮颜色 const MULTIPLE_THRESHOLD = 5; // 倍数阈值 (例如,5代表5倍) const INCLUDE_EQUAL = true; // 如果价格等于阈值倍数,是否也高亮 (true为高亮,false为严格大于) const DEBOUNCE_DELAY = 300; // 防抖延迟,单位毫秒 (避免频繁更新) let debounceTimer; // 用于防抖的定时器 // 存储 SKU 名称到其对应元素(价格输入、复选框)的映射,提高查找效率和准确性 let skuElementsMap = new Map(); // Key: SKU Name (as string), Value: { priceInput: HTMLElement, checkboxInput: HTMLElement } /** * 清理 SKU 名称,去除不必要的空格或特殊字符,使其更适合匹配 * @param {string} name - 原始 SKU 名称 * @returns {string} - 清理后的 SKU 名称 */ function cleanSkuName(name) { // 使用正则表达式替换所有空白字符(包括空格、制表符、换行符等)为一个空格,并去除首尾空格 return name ? name.replace(/\s+/g, ' ').trim() : ''; } /** * 获取所有有效的 SKU 价格输入框,并建立 SKU 名称与元素(价格输入、复选框)的映射 * @returns {Array} 包含 { name, price, priceInput, checkboxInput } 的数组 */ function getSkuData() { console.log('[Shopee Price Highlighter & Filter] Collecting SKU data...'); const priceInputs = document.querySelectorAll('#skuDataInfo tbody tr td:nth-child(4) input.g-form-component'); const skuData = []; skuElementsMap.clear(); // 每次收集前清空,确保最新状态 priceInputs.forEach((priceInput, index) => { const row = priceInput.closest('tr'); if (!row) return; let rawSkuName = ''; // 尝试从行中获取 SKU 名称 // 优先级: // 1. data-sku-name属性 // 2. 表格第二个td的直接文本内容 (根据最新反馈) // 3. 表格第一个td中的特定子元素 (span[title], input, .sku-name-text, .break-all) rawSkuName = row.dataset.skuName; // 尝试从自定义属性获取 if (!rawSkuName) { // 如果没有自定义属性,则从DOM解析 const secondTd = row.querySelector('td:nth-child(2)'); // 尝试获取第二个td if (secondTd && secondTd.textContent.trim()) { rawSkuName = secondTd.textContent.trim(); // 获取其直接文本内容 } else { // 如果第二个td没有内容,或者不存在,再尝试第一个td的子元素 (兼容旧版或其他布局) const firstTd = row.querySelector('td:first-child'); if (firstTd) { const skuNameEl = firstTd.querySelector('span[title]') || firstTd.querySelector('.ant-input[data-v-9bbdc5ab]') || firstTd.querySelector('.sku-name-text') || firstTd.querySelector('span.break-all'); rawSkuName = skuNameEl ? skuNameEl.title || skuNameEl.value || skuNameEl.textContent : ''; } } } const skuName = cleanSkuName(rawSkuName); // 如果SKU名称仍然为空,记录详细日志 if (!skuName) { console.warn(`[Shopee Price Highlighter & Filter] Failed to extract SKU name for row #${index}. Defaulting to "Unknown SKU". Row HTML:`, row.outerHTML); // 确保即使名称为空,也有一个可识别的placeholder const tempSkuName = 'Unknown SKU for Row ' + index; rawSkuName = tempSkuName; // 确保后面的 findCheckboxForSkuName 不会因为空字符串而崩溃 } // 查找对应的复选框 // 只有当 skuName 不是 "Unknown SKU" 并且有实际内容时才尝试匹配复选框 let checkboxInput = null; if (skuName && !skuName.startsWith('Unknown SKU')) { checkboxInput = findCheckboxForSkuName(skuName, index); } if (!checkboxInput && skuName && !skuName.startsWith('Unknown SKU')) { // 仅当未找到且 SKU 名称有效时记录警告 console.warn(`[Shopee Price Highlighter & Filter] No checkbox found for SKU (from price table) #${index}: "${skuName}"`); } const price = parseFloat(priceInput.value); if (!isNaN(price) && price > 0) { const data = { name: skuName, // 使用清理后的SKU名称 price: price, priceInput: priceInput, checkboxInput: checkboxInput // 可能为 null }; skuData.push(data); // 存储到 map 中,key 使用清理后的 SKU 名称 skuElementsMap.set(skuName, { priceInput: priceInput, checkboxInput: checkboxInput }); } }); console.log(`[Shopee Price Highlighter & Filter] Found ${skuData.length} valid SKUs in price table. ${skuData.filter(s => s.checkboxInput).length} SKUs successfully linked to checkboxes.`); return skuData; } /** * 根据 SKU 名称查找对应的复选框 * 优化查找逻辑,使其更健壮 */ function findCheckboxForSkuName(skuName, skuIndexForLog = -1) { if (!skuName || skuName.startsWith('Unknown SKU')) { // 修正:如果skuName是空的或者'Unknown SKU',直接返回null console.warn(`[Shopee Price Highlighter & Filter] findCheckboxForSkuName called with empty or generic skuName ("${skuName}") for index #${skuIndexForLog}. Cannot match checkbox.`); return null; } let foundCheckbox = null; // 1. 尝试精确匹配 input[value] 属性 (最理想情况) foundCheckbox = document.querySelector(`.checkbox-group input[type="checkbox"][value="${skuName}"]`); if (foundCheckbox) { console.log(`[Shopee Price Highlighter & Filter] SKU #${skuIndexForLog} "${skuName}" matched checkbox by exact 'value' attribute.`); return foundCheckbox; } // 2. 尝试遍历所有 checkbox-item,精确匹配其内部 theme-value-text 的 title 属性 const allCheckboxItems = document.querySelectorAll('.checkbox-group .checkbox-item'); for (let i = 0; i < allCheckboxItems.length; i++) { const item = allCheckboxItems[i]; const cb = item.querySelector('input[type="checkbox"]'); const textEl = item.querySelector('.theme-value-edit .theme-value-text'); if (cb && textEl) { // 清理并比较 title 属性 if (cleanSkuName(textEl.title) === skuName) { console.log(`[Shopee Price Highlighter & Filter] SKU #${skuIndexForLog} "${skuName}" matched checkbox by exact 'title' attribute: "${textEl.title}"`); return cb; } } } // 3. 尝试遍历所有 checkbox-item,模糊匹配其内部 theme-value-text 的 textContent (最宽松,但也最容易误判) // 只有当精确匹配都失败时才尝试 for (let i = 0; i < allCheckboxItems.length; i++) { const item = allCheckboxItems[i]; const cb = item.querySelector('input[type="checkbox"]'); const textEl = item.querySelector('.theme-value-edit .theme-value-text'); if (cb && textEl) { const cleanedTextContent = cleanSkuName(textEl.textContent); // 检查 SKU 名称是否包含在文本内容中,或者文本内容是否包含在 SKU 名称中 // 避免 'Unknown SKU' 与实际内容模糊匹配,因为 'Unknown SKU' 本身就是个错误 if (cleanedTextContent && skuName && (cleanedTextContent.includes(skuName) || skuName.includes(cleanedTextContent))) { console.warn(`[Shopee Price Highlighter & Filter] SKU #${skuIndexForLog} "${skuName}" fuzzy matched checkbox by textContent: "${cleanedTextContent}"`); return cb; } } } return null; } /** * 查找并高亮价格 (V1.4 版本的高亮逻辑) */ function highlightPrices() { console.log('[Shopee Price Highlighter & Filter] Executing price highlight (V1.4 Logic)...'); const skuData = getSkuData(); // 重新获取最新 SKU 数据 if (skuData.length === 0) { console.log('[Shopee Price Highlighter & Filter] No SKU data for highlighting.'); return; } const validPrices = skuData.map(s => s.price).filter(price => !isNaN(price) && price > 0); if (validPrices.length === 0) { console.log('[Shopee Price Highlighter & Filter] No valid prices for comparison. Clearing highlights.'); // 清除所有高亮 skuData.forEach(sku => { sku.priceInput.style.backgroundColor = ''; }); return; } const minPrice = Math.min(...validPrices); const thresholdPrice = minPrice * MULTIPLE_THRESHOLD; console.log(`[Shopee Price Highlighter & Filter] Global Min Price (from all valid SKUs): ${minPrice}, Threshold: ${thresholdPrice} (${MULTIPLE_THRESHOLD}x Min)`); skuData.forEach(sku => { sku.priceInput.style.backgroundColor = ''; // 清除之前的高亮 if (!isNaN(sku.price)) { if (INCLUDE_EQUAL) { if (sku.price >= thresholdPrice) { sku.priceInput.style.backgroundColor = HIGHLIGHT_COLOR; } } else { if (sku.price > thresholdPrice) { sku.priceInput.style.backgroundColor = HIGHLIGHT_COLOR; } } } }); } /** * 模拟点击复选框以改变其状态,并触发 change 事件 * @param {HTMLElement} checkboxInput - 复选框元素 * @param {boolean} checked - 期望的状态 (true 为勾选,false 为取消勾选) */ function setCheckboxState(checkboxInput, checked) { if (!checkboxInput) { console.warn('[Shopee Price Highlighter & Filter] Attempted to set state for null checkboxInput.'); return; } if (checkboxInput.checked === checked) { return; // 已经处于期望状态 } checkboxInput.checked = checked; // 模拟触发 change 事件,让店小秘的内部逻辑响应 const event = new Event('change', { bubbles: true }); checkboxInput.dispatchEvent(event); // 获取对应的SKU名称用于日志 let skuNameForLog = cleanSkuName(checkboxInput.value); // 优先使用 checkbox 的 value const item = checkboxInput.closest('.checkbox-item'); if (item) { const titleSpan = item.querySelector('.theme-value-text'); if (titleSpan) skuNameForLog = cleanSkuName(titleSpan.title || titleSpan.textContent); } console.log(`[Shopee Price Highlighter & Filter] Checkbox for "${skuNameForLog}" set to ${checked}`); } /** * 去掉最低价 SKU */ function removeLowestPriceSku() { // 这里仍然只考虑当前勾选的 SKU 进行操作,因为我们只希望操作用户当前可见且激活的 SKU const skuData = getSkuData().filter(s => s.checkboxInput && s.checkboxInput.checked); if (skuData.length <= 1) { alert('至少需要保留一个 SKU。'); return; } const minPrice = Math.min(...skuData.map(s => s.price)); // 找到所有最低价的SKU(可能不止一个),取第一个 const lowestSku = skuData.find(s => s.price === minPrice); if (lowestSku) { setCheckboxState(lowestSku.checkboxInput, false); console.log(`[Shopee Price Highlighter & Filter] Removed lowest price SKU: ${lowestSku.name} (${lowestSku.price})`); // 等待 DOM 更新,然后重新高亮 setTimeout(highlightPrices, DEBOUNCE_DELAY); } else { console.log('[Shopee Price Highlighter & Filter] No lowest price active SKU found to remove.'); } } /** * 去掉最高价 SKU */ function removeHighestPriceSku() { const skuData = getSkuData().filter(s => s.checkboxInput && s.checkboxInput.checked); // 只考虑当前勾选的 if (skuData.length <= 1) { alert('至少需要保留一个 SKU。'); return; } const maxPrice = Math.max(...skuData.map(s => s.price)); // 找到所有最高价的SKU(可能不止一个),取第一个 const highestSku = skuData.find(s => s.price === maxPrice); if (highestSku) { setCheckboxState(highestSku.checkboxInput, false); console.log(`[Shopee Price Highlighter & Filter] Removed highest price SKU: ${highestSku.name} (${highestSku.price})`); // 等待 DOM 更新,然后重新高亮 setTimeout(highlightPrices, DEBOUNCE_DELAY); } else { console.log('[Shopee Price Highlighter & Filter] No highest price active SKU found to remove.'); } } // --- 启动 MutationObserver 实时监听DOM变化 --- const observerCallback = function(mutationsList, observer) { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { highlightPrices(); }, DEBOUNCE_DELAY); }; let observerRetryCount = 0; const maxObserverRetries = 10; const observerRetryInterval = 500; // 500ms function initializeObserver() { const targetNode = document.getElementById('skuDataInfo'); if (targetNode) { const config = { childList: true, // 观察子节点的增加或删除 (添加/删除 SKU 行) subtree: true, // 观察所有后代节点 (价格输入框的 value 变化) attributes: true, // 观察属性变化 attributeFilter: ['value'] // 只监听value属性的变化,优化性能 }; const observer = new MutationObserver(observerCallback); observer.observe(targetNode, config); console.log('[Shopee Price Highlighter & Filter] MutationObserver started, observing #skuDataInfo.'); highlightPrices(); // 首次加载时执行一次高亮 } else if (observerRetryCount < maxObserverRetries) { observerRetryCount++; console.warn(`[Shopee Price Highlighter & Filter] #skuDataInfo element not found on initial load. Retrying (${observerRetryCount}/${maxObserverRetries})...`); setTimeout(initializeObserver, observerRetryInterval); } else { console.error('[Shopee Price Highlighter & Filter] Failed to find #skuDataInfo after multiple retries. Script may not function correctly.'); } } /** * 添加功能按钮 */ let addButtonsRetryCount = 0; const maxAddButtonsRetries = 10; const addButtonsRetryInterval = 500; // 500ms function addFilterButtons() { if (document.getElementById('skuFilterButtons')) { // 按钮已经存在,不再重复添加 console.log('[Shopee Price Highlighter & Filter] Filter buttons already exist. Skipping addFilterButtons.'); return; } // 精确瞄准用户指定的 div 元素 const targetElementForButtons = document.querySelector("#skuDataInfo > div.form-card-content > div > div > div.mb-20.flex-justify-between"); if (targetElementForButtons) { const buttonContainer = document.createElement('div'); buttonContainer.id = 'skuFilterButtons'; // 利用父元素的 flex-justify-between 样式,将按钮推到右侧 buttonContainer.style.cssText = 'display: flex; gap: 10px; align-items: center;'; const createButton = (text, onClickHandler) => { const button = document.createElement('button'); button.className = 'ant-btn ant-btn-default css-1oz1bg8'; // 使用店小秘的按钮样式 button.textContent = text; button.style.backgroundColor = '#e6f7ff'; // 蓝色系,与店小秘主题更搭 button.style.borderColor = '#91d5ff'; button.style.color = '#1890ff'; button.style.minWidth = 'unset'; // 取消最小宽度限制,让按钮更紧凑 button.style.padding = '4px 12px'; // 调整内边距 button.style.height = '32px'; // 调整高度 button.addEventListener('click', onClickHandler); return button; }; buttonContainer.appendChild(createButton('移除最低价', removeLowestPriceSku)); buttonContainer.appendChild(createButton('移除最高价', removeHighestPriceSku)); // 直接追加到目标 div 内部 targetElementForButtons.appendChild(buttonContainer); console.log('[Shopee Price Highlighter & Filter] Filter buttons successfully added to variant count section.'); addButtonsRetryCount = 0; // 重置重试计数器 } else if (addButtonsRetryCount < maxAddButtonsRetries) { addButtonsRetryCount++; console.warn(`[Shopee Price Highlighter & Filter] Target element for filter buttons not found yet. Retrying (${addButtonsRetryCount}/${maxAddButtonsRetries})...`); setTimeout(addFilterButtons, addButtonsRetryInterval); } else { console.error('[Shopee Price Highlighter & Filter] Failed to find target element for filter buttons after multiple retries.'); } } // --- 启动脚本 --- function init() { initializeObserver(); addFilterButtons(); // 首次尝试添加按钮 // 额外的延迟执行,以防内容是异步加载的,确保按钮和高亮 setTimeout(addFilterButtons, 1500); // 1.5秒后再次尝试添加按钮 setTimeout(highlightPrices, 1500); // 1.5秒后再次尝试执行一次高亮 setTimeout(addFilterButtons, 4000); // 4秒后再次尝试添加按钮 setTimeout(highlightPrices, 4000); // 4秒后再次尝试执行一次高亮 } // 等待 DOMContentLoaded 事件确保页面基本结构加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();