// ==UserScript== // @name 店小蜜5倍价格筛选 // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description 5倍价格筛选 // @author Rayu // @match https://www.dianxiaomi.com/web/shopeeSite/* // @exclude https://www.dianxiaomi.com/web/shopeeSite/edit* // @grant none // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 等待页面加载完成 function waitForElement(selector, callback, maxWait = 10000) { const startTime = Date.now(); const interval = setInterval(() => { const element = document.querySelector(selector); if (element) { clearInterval(interval); callback(); } else if (Date.now() - startTime > maxWait) { clearInterval(interval); console.log('等待元素超时'); } }, 500); } // ================== 数据持久化缓存模块 ================== const CACHE_KEY = 'dianxiaomi_product_cache'; const CACHE_EXPIRE_DAYS = 7; // 缓存过期天数 // 缓存管理器 const CacheManager = { // 保存数据到localStorage save(data) { try { const cacheData = { data: data, timestamp: Date.now(), version: '1.0.0' }; localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData)); console.log('💾 数据已保存到localStorage,当前数量:', data.length); } catch (e) { console.error('❌ 保存缓存失败:', e); } }, // 从localStorage加载数据 load() { try { const cached = localStorage.getItem(CACHE_KEY); if (!cached) { console.log('📂 未找到缓存数据,初始化为空数组'); return []; } const cacheData = JSON.parse(cached); const age = Date.now() - cacheData.timestamp; const ageDays = Math.floor(age / (1000 * 60 * 60 * 24)); console.log(`📂 加载缓存数据: ${cacheData.data.length} 条记录`); console.log(`⏰ 缓存时间: ${ageDays} 天前`); return cacheData.data || []; } catch (e) { console.error('❌ 加载缓存失败:', e); return []; } }, // 清除所有缓存 clear() { try { localStorage.removeItem(CACHE_KEY); console.log('🗑️ 已清除localStorage中的所有缓存'); } catch (e) { console.error('❌ 清除缓存失败:', e); } }, // 清理过期缓存(超过7天) cleanExpired() { try { const cached = localStorage.getItem(CACHE_KEY); if (!cached) return; const cacheData = JSON.parse(cached); const age = Date.now() - cacheData.timestamp; const ageDays = age / (1000 * 60 * 60 * 24); if (ageDays > CACHE_EXPIRE_DAYS) { this.clear(); console.log(`🧹 自动清理 ${Math.floor(ageDays)} 天前的过期缓存`); return true; } return false; } catch (e) { console.error('❌ 清理过期缓存失败:', e); return false; } }, // 获取缓存信息 getInfo() { try { const cached = localStorage.getItem(CACHE_KEY); if (!cached) { return { exists: false, count: 0, ageDays: 0 }; } const cacheData = JSON.parse(cached); const age = Date.now() - cacheData.timestamp; const ageDays = Math.floor(age / (1000 * 60 * 60 * 24)); return { exists: true, count: cacheData.data?.length || 0, ageDays: ageDays, timestamp: cacheData.timestamp }; } catch (e) { console.error('❌ 获取缓存信息失败:', e); return { exists: false, count: 0, ageDays: 0 }; } } }; // 页面加载时自动清理过期缓存 CacheManager.cleanExpired(); // 从localStorage加载已缓存的数据 let capturedApiData = CacheManager.load(); // 将capturedApiData和缓存管理器暴露到全局作用域 window.capturedApiData = capturedApiData; window.CacheManager = CacheManager; console.log('🚀 API拦截器已启动'); console.log('💡 提示: 可以在控制台使用 window.capturedApiData 或 capturedApiData 访问捕获的数据'); // ================== 分类数据持久化模块 ================== const CATEGORY_CACHE_KEY = 'dianxiaomi_categories'; // 分类数据管理器 const CategoryManager = { // 保存分类数据到localStorage save(categories) { try { const cacheData = { categories: categories, lastUpdate: Date.now(), version: '1.0.0' }; localStorage.setItem(CATEGORY_CACHE_KEY, JSON.stringify(cacheData)); console.log('💾 [分类] 数据已保存到localStorage,分类数量:', categories.length); return true; } catch (e) { console.error('❌ [分类] 保存失败:', e); return false; } }, // 从localStorage加载分类数据 load() { try { const cached = localStorage.getItem(CATEGORY_CACHE_KEY); if (!cached) { console.log('📂 [分类] 未找到缓存数据'); return null; } const cacheData = JSON.parse(cached); const age = Date.now() - cacheData.lastUpdate; const ageDays = Math.floor(age / (1000 * 60 * 60 * 24)); console.log(`📂 [分类] 加载缓存数据: ${cacheData.categories.length} 个分类`); console.log(`⏰ [分类] 缓存时间: ${ageDays} 天前`); return cacheData.categories || []; } catch (e) { console.error('❌ [分类] 加载失败:', e); return null; } }, // 检查是否存在缓存 exists() { try { const cached = localStorage.getItem(CATEGORY_CACHE_KEY); return !!cached; } catch (e) { return false; } }, // 对比并更新分类数据 updateIfChanged(newCategories) { const oldCategories = this.load() || []; // 创建ID映射用于对比 const oldMap = new Map(oldCategories.map(c => [c.id, c.name])); const newMap = new Map(newCategories.map(c => [c.id, c.name])); let hasChanges = false; const changes = []; // 检查新增或修改 for (const [id, name] of newMap) { if (!oldMap.has(id)) { changes.push(`新增: ${name} (${id})`); hasChanges = true; } else if (oldMap.get(id) !== name) { changes.push(`修改: ${oldMap.get(id)} → ${name} (${id})`); hasChanges = true; } } // 检查删除 for (const [id, name] of oldMap) { if (!newMap.has(id)) { changes.push(`删除: ${name} (${id})`); hasChanges = true; } } if (hasChanges) { console.log('🔄 [分类] 检测到数据变化:'); changes.forEach(change => console.log(` ${change}`)); this.save(newCategories); return true; } else { console.log('✓ [分类] 数据无变化,跳过保存'); return false; } } }; // 暴露分类管理器到全局 window.CategoryManager = CategoryManager; // 显示首次使用提示 function showFirstTimeGuide() { const modal = document.createElement('div'); modal.id = 'category-first-time-guide'; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); display: flex; align-items: center; justify-content: center; z-index: 999999; `; const dialog = document.createElement('div'); dialog.style.cssText = ` background: white; border-radius: 12px; padding: 32px; width: 480px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); text-align: center; `; dialog.innerHTML = `
📦
首次使用提示
检测到您尚未初始化分类数据
请手动移动一次分类,系统将自动捕获并保存分类列表
完成后即可正常使用批量移动功能
💡 操作步骤:
1. 在商品列表中选择任意商品
2. 点击「移动分类」按钮
3. 选择目标分类并确认
4. 系统将自动保存分类数据
`; modal.appendChild(dialog); document.body.appendChild(modal); console.log('📢 [分类] 显示首次使用引导'); // 关闭按钮事件 const closeBtn = dialog.querySelector('#close-guide-btn'); closeBtn.addEventListener('click', () => { if (document.body.contains(modal)) { document.body.removeChild(modal); } console.log('ℹ️ [分类] 用户手动关闭引导'); }); // 鼠标悬停效果 closeBtn.addEventListener('mouseenter', () => { closeBtn.style.background = '#40a9ff'; }); closeBtn.addEventListener('mouseleave', () => { closeBtn.style.background = '#1890ff'; }); // 返回Promise,当分类数据保存后自动关闭 return new Promise((resolve) => { const checkInterval = setInterval(() => { if (CategoryManager.exists()) { clearInterval(checkInterval); if (document.body.contains(modal)) { document.body.removeChild(modal); } console.log('✅ [分类] 首次使用引导已完成'); resolve(); } }, 500); }); } // 页面加载时检查分类数据 window.addEventListener('load', function() { setTimeout(() => { if (!CategoryManager.exists()) { console.log('⚠️ [分类] 首次使用,显示引导提示'); showFirstTimeGuide(); } else { console.log('✅ [分类] 分类数据已存在,跳过引导'); } }, 1000); }); // 公共函数:处理分类移动API响应 function handleCategoryApiResponse(data, source) { console.log(`🏷️ [分类] API响应完整数据 (${source}):`, JSON.stringify(data, null, 2)); let categoryList = null; // 尝试从 data.msg 解析 JSON 字符串 if (data && data.msg && typeof data.msg === 'string') { try { categoryList = JSON.parse(data.msg); console.log('🎯 [分类] 从msg字段解析到分类列表:', categoryList); } catch (e) { console.error('❌ [分类] 解析msg字段失败:', e); } } // 尝试 data.data 数组 else if (data && Array.isArray(data.data)) { categoryList = data.data; } // 尝试直接从 data 获取数组 else if (Array.isArray(data)) { categoryList = data; } if (Array.isArray(categoryList) && categoryList.length > 0) { console.log(`🎯 [分类] 提取到 ${categoryList.length} 个分类`); // 格式化新的分类数据 const newCategories = categoryList .filter(cat => cat.id && cat.name) .map(cat => ({ id: cat.id, name: cat.name })); // 加载现有分类数据进行对比 const oldCategories = CategoryManager.load() || []; // 检查数据是否相同 const isSame = oldCategories.length === newCategories.length && oldCategories.every((oldCat, index) => { const newCat = newCategories.find(c => c.id === oldCat.id); return newCat && newCat.name === oldCat.name; }); if (!isSame) { // 数据不同,直接替换 CategoryManager.save(newCategories); console.log(`✅ [分类] 检测到数据变化,已替换为新数据,总数: ${newCategories.length}`); } else { console.log(`ℹ️ [分类] 数据无变化,跳过保存`); } } else { console.warn('⚠️ [分类] 无法从API响应中提取分类列表,请检查数据结构'); console.log('💡 [分类] 完整响应数据:', data); } } // 公共函数:添加悬停效果 function addHoverEffect(element, hoverColor, normalColor) { element.addEventListener('mouseenter', function() { this.style.backgroundColor = hoverColor; }); element.addEventListener('mouseleave', function() { this.style.backgroundColor = normalColor; }); } // 拦截XMLHttpRequest const originalXHROpen = XMLHttpRequest.prototype.open; const originalXHRSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this._url = url; this._method = method; return originalXHROpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function(body) { const xhr = this; const requestBody = body; this.addEventListener('load', function() { try { if (this._url && this.responseText) { // 检查是否是目标API const isTargetApi = this._url.includes('shopeeProduct') || this._url.includes('getProductById'); // 检查是否是分类移动API const isCategoryMoveApi = this._url.includes('batchChangeCategory.json'); if (isTargetApi) { console.log('🎯 捕获到目标API (XHR):', this._url); } if (isCategoryMoveApi) { console.log('🏷️ [分类] 捕获到分类移动API (XHR):', this._url); } // 尝试解析JSON响应 const data = JSON.parse(this.responseText); console.log('🔍 XHR请求:', this._url); console.log('📦 响应数据:', data); // 处理分类移动API if (isCategoryMoveApi) { handleCategoryApiResponse(data, 'XHR'); } capturedApiData.push({ url: this._url, method: 'XHR', data: data, timestamp: new Date().toISOString() }); // 自动保存到localStorage CacheManager.save(capturedApiData); if (isTargetApi) { console.log('✅ 已保存到 capturedApiData,当前总数:', capturedApiData.length); } } } catch (e) { // 忽略非JSON响应 if (this._url && (this._url.includes('shopeeProduct') || this._url.includes('getProductById'))) { console.error('❌ 解析目标API响应失败:', this._url, e); } } }); this.addEventListener('error', function() { console.error('❌ XHR请求失败:', this._url); }); return originalXHRSend.apply(this, arguments); }; // 拦截fetch const originalFetch = window.fetch; window.fetch = function() { const url = arguments[0]; const isTargetApi = typeof url === 'string' && (url.includes('shopeeProduct') || url.includes('getProductById')); const isCategoryMoveApi = typeof url === 'string' && url.includes('batchChangeCategory.json'); if (isTargetApi) { console.log('🎯 准备发起目标API请求 (Fetch):', url); } if (isCategoryMoveApi) { console.log('🏷️ [分类] 捕获到分类移动API (Fetch):', url); } return originalFetch.apply(this, arguments).then(response => { // 克隆响应以便读取 const clonedResponse = response.clone(); clonedResponse.json().then(data => { console.log('🔍 Fetch请求:', url); console.log('📦 响应数据:', data); // 处理分类移动API if (isCategoryMoveApi) { handleCategoryApiResponse(data, 'Fetch'); } capturedApiData.push({ url: url, method: 'Fetch', data: data, timestamp: new Date().toISOString() }); // 自动保存到localStorage CacheManager.save(capturedApiData); if (isTargetApi) { console.log('✅ 已保存到 capturedApiData,当前总数:', capturedApiData.length); } }).catch((e) => { // 忽略非JSON响应 if (isTargetApi) { console.error('❌ 解析目标API响应失败:', url, e); } }); return response; }).catch(error => { console.error('❌ Fetch请求失败:', url, error); throw error; }); }; // 从API数据中提取商品信息(新格式) function extractProductInfo() { const products = []; console.log('开始从API数据中提取商品信息...'); console.log(`已捕获 ${capturedApiData.length} 个API请求`); // 遍历所有捕获的API数据 capturedApiData.forEach((apiCall, index) => { console.log(`\n处理API请求 ${index + 1}:`, apiCall.url); // 判断API类型 const isGetProductById = apiCall.url.includes('getProductById'); console.log(`🔖 API类型: ${isGetProductById ? 'getProductById (规格列表)' : 'pageList (商品列表)'}`); // 如果是getProductById API,直接提取variations数组 if (isGetProductById && apiCall.data && apiCall.data.data && Array.isArray(apiCall.data.data)) { console.log(`📦 发现 getProductById 数据,包含 ${apiCall.data.data.length} 个规格`); // 从第一个variation中获取商品基本信息 const firstVariation = apiCall.data.data[0]; if (firstVariation) { const product = { name: firstVariation.dxmProductName || '(从规格数据获取)', id: firstVariation.dxmProductId || firstVariation.id, // 保留原始ID用于数据合并 idStr: firstVariation.idStr || firstVariation.id, // 用于UI显示 dxmProductId: firstVariation.dxmProductId || firstVariation.id, specifications: [] }; // 提取所有规格 apiCall.data.data.forEach((variation, vIndex) => { // 组合 option1 和 option2 const optionText = [variation.option1, variation.option2].filter(Boolean).join(' - '); console.log(` [${vIndex + 1}] ${optionText}: ¥${variation.price} (idStr: ${variation.idStr || '无'})`); if ((variation.option1 !== undefined || variation.option2 !== undefined) && variation.price !== undefined) { product.specifications.push({ option: optionText, price: variation.price, idStr: variation.idStr || variation.id }); } }); if (product.specifications.length > 0) { console.log(`✅ 从 getProductById 提取商品: ${product.name} (DXM ID: ${product.dxmProductId})`); console.log(` └─ 规格数量: ${product.specifications.length}`); products.push(product); } } // getProductById API处理完毕,跳过递归查找 return; } // 对于pageList API,使用递归查找 // 递归查找数据中的商品信息 function findProducts(obj, path = '') { if (!obj || typeof obj !== 'object') return; // 检查当前对象是否包含商品信息(必须有name和id) if (obj.name && obj.id) { console.log(`\n🔍 发现潜在商品对象: ${obj.name} (ID: ${obj.id})`); console.log(` ├─ 路径: ${path}`); console.log(` ├─ idStr: ${obj.idStr || '无'}`); console.log(` ├─ dxmProductId: ${obj.dxmProductId || '无'}`); console.log(` ├─ variations 类型: ${Array.isArray(obj.variations) ? '数组' : typeof obj.variations}`); console.log(` └─ variations 长度: ${Array.isArray(obj.variations) ? obj.variations.length : 'N/A'}`); const product = { name: obj.name, id: obj.id, // 保留原始ID用于数据合并 idStr: obj.idStr || obj.id, // 用于UI显示 dxmProductId: obj.idStr || obj.dxmProductId || obj.id, // 优先使用idStr specifications: [] }; // 提取variations数组 if (Array.isArray(obj.variations)) { console.log(` 📦 开始处理 ${obj.variations.length} 个 variations...`); obj.variations.forEach((variation, vIndex) => { // 组合 option1 和 option2 const optionText = [variation.option1, variation.option2].filter(Boolean).join(' - '); console.log(` [${vIndex + 1}] option1=${variation.option1}, option2=${variation.option2 || '无'}, price=${variation.price}, dxmProductId=${variation.dxmProductId || '无'}`); if ((variation.option1 !== undefined || variation.option2 !== undefined) && variation.price !== undefined) { product.specifications.push({ option: optionText, price: variation.price }); } }); console.log(` ✅ 成功提取 ${product.specifications.length} 个规格`); } else { console.log(` ⚠️ variations 不是数组或不存在`); // 输出整个对象的键,帮助调试 console.log(` 📋 对象的所有键:`, Object.keys(obj).join(', ')); } // dxmProductId已在创建product时设置(优先级:idStr > dxmProductId > id) console.log(` ✓ 使用的 dxmProductId: ${product.dxmProductId}`); // 只有当有规格数据时才添加商品 if (product.specifications.length > 0) { console.log(`✓ 找到商品: ${product.name} (ID: ${product.id}, DXM ID: ${product.dxmProductId || '无'})`); console.log(` └─ 规格数量: ${product.specifications.length}`); console.log(` └─ 路径: ${path}`); products.push(product); console.log(` ⚙️ 调试模式: 继续递归查找所有嵌套对象`); } else { console.log(` ❌ 该商品没有有效的规格数据,跳过`); } } // 递归遍历对象和数组 if (Array.isArray(obj)) { obj.forEach((item, idx) => findProducts(item, `${path}[${idx}]`)); } else { // 遍历对象的所有属性 for (let key in obj) { if (obj.hasOwnProperty(key)) { findProducts(obj[key], path ? `${path}.${key}` : key); } } } } findProducts(apiCall.data); }); console.log(`\n共找到 ${products.length} 个商品(去重前)`); // 去重:合并相同ID的商品数据 const productMap = new Map(); products.forEach(product => { const key = product.id; // 使用原始ID作为唯一标识符 if (productMap.has(key)) { // 如果已存在该商品,合并规格数据 const existing = productMap.get(key); console.log(`🔄 发现重复商品: ${product.name} (ID: ${key})`); console.log(` 合并前规格数量: ${existing.specifications.length}`); console.log(` 待合并规格数量: ${product.specifications.length}`); // 合并规格数组(去重相同option的规格) const specMap = new Map(); existing.specifications.forEach(spec => { specMap.set(spec.option, spec); }); product.specifications.forEach(spec => { if (!specMap.has(spec.option)) { specMap.set(spec.option, spec); } }); existing.specifications = Array.from(specMap.values()); console.log(` 合并后规格数量: ${existing.specifications.length}`); } else { // 首次出现该商品,直接添加 productMap.set(key, product); } }); const mergedProducts = Array.from(productMap.values()); console.log(`\n✅ 去重后共 ${mergedProducts.length} 个商品`); return mergedProducts; } // 显示商品数据 function displayProductData(products) { console.log('\n=== 商品信息数据 ==='); console.log(`共找到 ${products.length} 个商品`); console.log('\n详细列表:'); products.forEach((product, index) => { console.log(`${index + 1}. 商品: ${product.name}`); console.log(` ID: ${product.id}`); console.log(` DXM ID: ${product.dxmProductId}`); console.log(` 规格数量: ${product.specifications.length} 个`); product.specifications.forEach((spec, sIndex) => { console.log(` [${sIndex + 1}] ${spec.option}: ${spec.price}`); }); console.log('---'); }); return products; } // 创建浮动窗口UI function createFloatingPanel() { // 创建容器 const panel = document.createElement('div'); panel.id = 'dianxiaomi-price-panel'; panel.style.cssText = ` position: fixed; top: 10px; right: 10px; width: 400px; max-height: 600px; background: white; border: 2px solid #1890ff; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 999999; font-family: Arial, sans-serif; overflow: hidden; display: flex; flex-direction: column; `; // 创建标题栏 const header = document.createElement('div'); header.style.cssText = ` background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 12px 15px; font-weight: bold; font-size: 16px; display: flex; justify-content: space-between; align-items: center; cursor: move; `; header.innerHTML = ` 📊 商品价格数据 `; // 创建工具栏 const toolbar = document.createElement('div'); toolbar.style.cssText = ` padding: 10px 15px; background: #f5f5f5; border-bottom: 1px solid #e0e0e0; display: flex; gap: 10px; `; toolbar.innerHTML = ` 准备就绪 `; // 创建内容区域 const content = document.createElement('div'); content.id = 'dianxiaomi-content'; content.style.cssText = ` flex: 1; overflow-y: auto; padding: 15px; max-height: 500px; `; content.innerHTML = '
点击"手动获取"按钮开始获取数据
'; // 组装面板 panel.appendChild(header); panel.appendChild(toolbar); panel.appendChild(content); document.body.appendChild(panel); // 添加拖拽功能 let isDragging = false; let currentX; let currentY; let initialX; let initialY; header.addEventListener('mousedown', (e) => { if (e.target.id === 'dianxiaomi-close-btn') return; isDragging = true; initialX = e.clientX - panel.offsetLeft; initialY = e.clientY - panel.offsetTop; }); document.addEventListener('mousemove', (e) => { if (isDragging) { e.preventDefault(); currentX = e.clientX - initialX; currentY = e.clientY - initialY; panel.style.left = currentX + 'px'; panel.style.top = currentY + 'px'; panel.style.right = 'auto'; } }); document.addEventListener('mouseup', () => { isDragging = false; }); // 关闭按钮事件 document.getElementById('dianxiaomi-close-btn').addEventListener('click', () => { panel.style.display = 'none'; }); // 刷新按钮事件 document.getElementById('dianxiaomi-refresh-btn').addEventListener('click', () => { updatePanelData(true); // 自动展开 }); // 清除缓存按钮事件 document.getElementById('dianxiaomi-clear-btn').addEventListener('click', () => { // 清空内存中的数组 capturedApiData.length = 0; // 清除localStorage中的缓存 CacheManager.clear(); console.log('🗑️ 已清除所有缓存数据(内存 + localStorage)'); console.log('💡 当前 capturedApiData.length =', window.capturedApiData.length); const contentEl = document.getElementById('dianxiaomi-content'); const statusEl = document.getElementById('dianxiaomi-status'); contentEl.innerHTML = '
✓ 缓存已清除
内存和localStorage数据已全部清空
点击"手动获取"重新加载数据
'; statusEl.textContent = '缓存已清除'; statusEl.style.color = '#52c41a'; // 隐藏移动分类按钮 document.getElementById('dianxiaomi-move-category-btn').style.display = 'none'; }); // ================== 分类提取和选择功能 ================== // 获取分类列表(从本地缓存读取) function getCategories() { // 从localStorage加载分类数据 const categories = CategoryManager.load() || []; if (categories.length > 0) { console.log(`✅ 使用本地缓存的分类数据: ${categories.length} 个分类`); } else { console.log(`⚠️ 本地缓存中没有分类数据`); } return categories; } // 显示分类选择对话框 function showCategorySelector(categories, productCount) { return new Promise((resolve) => { // 创建模态对话框 const modal = document.createElement('div'); modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 999999; `; const dialog = document.createElement('div'); dialog.style.cssText = ` background: white; border-radius: 8px; padding: 0; width: 500px; max-height: 600px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); display: flex; flex-direction: column; `; // 对话框标题 const header = document.createElement('div'); header.style.cssText = ` padding: 16px 24px; border-bottom: 1px solid #f0f0f0; font-weight: bold; font-size: 16px; `; header.textContent = `选择目标分类 (将移动 ${productCount} 个商品)`; // 搜索框 const searchBox = document.createElement('input'); searchBox.type = 'text'; searchBox.placeholder = '搜索分类名称...'; searchBox.style.cssText = ` margin: 16px 24px; padding: 8px 12px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 14px; outline: none; `; // 分类列表容器 const listContainer = document.createElement('div'); listContainer.style.cssText = ` flex: 1; overflow-y: auto; padding: 0 24px; max-height: 400px; `; // 渲染分类列表 function renderCategories(filter = '') { listContainer.innerHTML = ''; const filtered = categories.filter(cat => cat.name.toLowerCase().includes(filter.toLowerCase()) || cat.id.toString().includes(filter) ); if (filtered.length === 0) { listContainer.innerHTML = '
未找到匹配的分类
'; return; } filtered.forEach(cat => { const item = document.createElement('div'); item.style.cssText = ` padding: 12px; margin-bottom: 8px; border: 1px solid #f0f0f0; border-radius: 4px; cursor: pointer; transition: all 0.2s; `; item.innerHTML = `
${cat.name}
ID: ${cat.id}
`; item.onmouseenter = () => { item.style.background = '#f5f5f5'; item.style.borderColor = '#1890ff'; }; item.onmouseleave = () => { item.style.background = 'white'; item.style.borderColor = '#f0f0f0'; }; item.onclick = () => { document.body.removeChild(modal); resolve(cat.id); }; listContainer.appendChild(item); }); } renderCategories(); // 搜索功能 searchBox.oninput = (e) => { renderCategories(e.target.value); }; // 底部按钮 const footer = document.createElement('div'); footer.style.cssText = ` padding: 16px 24px; border-top: 1px solid #f0f0f0; text-align: right; `; const cancelBtn = document.createElement('button'); cancelBtn.textContent = '取消'; cancelBtn.style.cssText = ` padding: 8px 16px; border: 1px solid #d9d9d9; border-radius: 4px; background: white; cursor: pointer; font-size: 14px; `; cancelBtn.onclick = () => { document.body.removeChild(modal); resolve(null); }; footer.appendChild(cancelBtn); // 组装对话框 dialog.appendChild(header); dialog.appendChild(searchBox); dialog.appendChild(listContainer); dialog.appendChild(footer); modal.appendChild(dialog); document.body.appendChild(modal); // 点击背景关闭 modal.onclick = (e) => { if (e.target === modal) { document.body.removeChild(modal); resolve(null); } }; // 聚焦搜索框 searchBox.focus(); }); } // 存储捕获的API信息(已预配置) window.dxmBatchMoveAPI = { url: '/api/shopeeProduct/batchChangeCategory.json', method: 'POST', bodyTemplate: 'ids={{PRODUCT_IDS}}&fullCid={{CATEGORY_ID}}&dxmState=offline' }; // 尝试从localStorage加载用户自定义配置(如果有的话会覆盖默认配置) try { const savedConfig = localStorage.getItem('dxmBatchMoveAPI'); if (savedConfig) { window.dxmBatchMoveAPI = JSON.parse(savedConfig); console.log('📋 已加载自定义API配置'); } else { console.log('📋 使用默认API配置'); } } catch (e) { console.log('📋 使用默认API配置'); } // 移动分类按钮事件 - API直调版本 document.getElementById('dianxiaomi-move-category-btn').addEventListener('click', async () => { const btn = document.getElementById('dianxiaomi-move-category-btn'); // 获取所有高价差商品 const highPriceProducts = []; window.dianxiaomiProductData.forEach((product, index) => { if (product.specifications && product.specifications.length > 0) { const prices = product.specifications.map(s => s.price).filter(p => p > 0); if (prices.length > 1) { const minPrice = Math.min(...prices); const maxPrice = Math.max(...prices); const priceRatio = maxPrice / minPrice; if (priceRatio > 5) { highPriceProducts.push({ index: index, name: product.name, idStr: product.idStr, dxmProductId: product.dxmProductId }); } } } }); console.log(`🎯 找到 ${highPriceProducts.length} 个高价差商品`); if (highPriceProducts.length === 0) { alert('未找到高价差商品(价格比>5)'); return; } // 检查是否需要重新配置API(按住Ctrl键点击可重新配置) if (event.ctrlKey) { const reconfigure = confirm('🔧 是否要重新配置API信息?\n\n当前配置:\nURL: ' + window.dxmBatchMoveAPI.url + '\nMethod: ' + window.dxmBatchMoveAPI.method); if (!reconfigure) return; window.dxmBatchMoveAPI = null; } if (false && !window.dxmBatchMoveAPI) { // 第一次使用:引导用户配置API const guide = `📋 批量移动分类 - API配置向导 找到 ${highPriceProducts.length} 个高价差商品 ⚠️ 首次使用需要配置API信息: 1️⃣ 打开浏览器开发者工具(F12) 2️⃣ 切换到 Network(网络)标签 3️⃣ 手动勾选1-2个商品 4️⃣ 选择一个分类并点击确认 5️⃣ 在Network中找到批量移动的API请求 6️⃣ 复制请求URL和请求体(Request Payload) 7️⃣ 再次点击本按钮,粘贴API信息 💡 提示:要复制的商品ID列表:`; const productIds = highPriceProducts.map(p => p.dxmProductId || p.idStr).filter(id => id).join('\n'); console.log('%c📋 高价差商品ID列表(用于测试):', 'color: #ff6b6b; font-weight: bold; font-size: 14px'); console.log(productIds); // 复制ID到剪贴板 navigator.clipboard.writeText(productIds).then(() => { alert(guide + '\n\n✅ 商品ID已复制到剪贴板!'); }).catch(() => { alert(guide + '\n\n' + productIds); }); // 提示用户输入API信息 const apiUrl = prompt('请输入批量移动API的URL\n(从Network面板复制):'); if (!apiUrl) return; const apiMethod = prompt('请输入API方法(GET/POST/PUT等):', 'POST'); if (!apiMethod) return; alert('请准备好请求体JSON格式\n(下一步粘贴)'); const apiBody = prompt('请输入请求体模板\n(JSON格式,商品ID用 {{PRODUCT_IDS}} 占位):'); if (!apiBody) return; // 保存API配置 window.dxmBatchMoveAPI = { url: apiUrl, method: apiMethod.toUpperCase(), bodyTemplate: apiBody }; // 保存到localStorage localStorage.setItem('dxmBatchMoveAPI', JSON.stringify(window.dxmBatchMoveAPI)); alert('✅ API配置已保存!\n\n请再次点击按钮执行批量移动'); return; } // 已配置API,执行批量移动 btn.textContent = '🔄 正在批量移动...'; btn.disabled = true; try { // 提取所有商品ID const productIds = highPriceProducts.map(p => p.dxmProductId || p.idStr).filter(id => id); console.log(`📤 准备批量移动 ${productIds.length} 个商品`); console.log('商品ID列表:', productIds); // 获取预配置的分类列表 const categories = getCategories(); // 显示分类选择对话框 const categoryId = await showCategorySelector(categories, productIds.length); if (!categoryId) { btn.textContent = '📁 移动分类'; btn.disabled = false; return; } // 替换请求体模板中的占位符 let requestBody = window.dxmBatchMoveAPI.bodyTemplate; // 将商品ID数组转换为逗号分隔的字符串(URL编码格式) const idsString = productIds.join(','); // 替换商品ID占位符 requestBody = requestBody.replace(/\{\{PRODUCT_IDS\}\}/g, encodeURIComponent(idsString)); // 替换分类ID占位符 requestBody = requestBody.replace(/\{\{CATEGORY_ID\}\}/g, encodeURIComponent(categoryId)); // 如果模板中没有占位符,说明是直接粘贴的请求体,需要手动构建 if (!window.dxmBatchMoveAPI.bodyTemplate.includes('{{')) { requestBody = `ids=${encodeURIComponent(idsString)}&fullCid=${encodeURIComponent(categoryId)}&dxmState=offline`; } console.log('📡 发送API请求...'); console.log('URL:', window.dxmBatchMoveAPI.url); console.log('Method:', window.dxmBatchMoveAPI.method); console.log('Body:', requestBody); // 构建完整的API URL const apiUrl = window.dxmBatchMoveAPI.url.startsWith('http') ? window.dxmBatchMoveAPI.url : window.location.origin + window.dxmBatchMoveAPI.url; // 发送API请求 const response = await fetch(apiUrl, { method: window.dxmBatchMoveAPI.method, headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest' }, body: requestBody, credentials: 'include' }); const result = await response.json(); console.log('📥 API响应:', result); if (response.ok && (result.success || result.code === 0 || result.code === 200)) { console.log(`✅ 批量移动成功!`); alert(`✅ 成功移动 ${productIds.length} 个商品到分类 ${categoryId}!\n\n请刷新页面查看结果`); btn.textContent = `✅ 已移动 ${productIds.length} 个`; } else { throw new Error(result.message || result.msg || '移动失败'); } } catch (error) { console.error('❌ 批量移动失败:', error); alert(`❌ 批量移动失败:\n\n${error.message}\n\n请检查:\n1. API配置是否正确\n2. 分类ID是否有效\n3. 是否有操作权限\n\n详细信息请查看控制台`); btn.textContent = '📁 移动分类'; } finally { btn.disabled = false; // 5秒后恢复按钮文本 setTimeout(() => { btn.textContent = '📁 移动分类'; }, 5000); } }); // 鼠标悬停效果 addHoverEffect(document.getElementById('dianxiaomi-refresh-btn'), '#1890ff', '#40a9ff'); addHoverEffect(document.getElementById('dianxiaomi-clear-btn'), '#ff4d4f', '#ff7875'); return panel; } // 自动展开所有商品 async function expandAllProducts() { console.log('🔍 开始查找展开按钮...'); // 定义所有可能的选择器 const selectors = [ 'td.col_10 span.link', 'td[class*="col_10"] span.link', '.vxe-body--column.col_10 span.link', 'span.link' ]; // 循环尝试所有选择器,找到第一个有结果的 let expandButtons = []; for (const selector of selectors) { expandButtons = document.querySelectorAll(selector); if (expandButtons.length > 0) break; } console.log(`✓ 找到 ${expandButtons.length} 个潜在展开按钮`); if (expandButtons.length === 0) { console.log('⚠️ 未找到展开按钮'); return 0; } // 输出第一个按钮的详细信息用于调试 if (expandButtons.length > 0) { const firstButton = expandButtons[0]; console.log('📋 第一个按钮详情:'); console.log(' - 标签:', firstButton.tagName); console.log(' - 类名:', firstButton.className); console.log(' - 内容:', firstButton.textContent?.trim() || '无文本'); console.log(' - 父元素:', firstButton.parentElement?.tagName); console.log(' - HTML:', firstButton.outerHTML.substring(0, 200)); } // 记录展开前的API数据数量 const beforeApiCount = capturedApiData.length; console.log(`📊 展开前已捕获 ${beforeApiCount} 个API请求`); let expandedCount = 0; // 批量点击所有展开按钮(使用真实的鼠标事件模拟) const clickPromises = []; for (let i = 0; i < expandButtons.length; i++) { const button = expandButtons[i]; // 检查按钮是否可见且可点击 if (button && button.offsetParent !== null) { try { // 模拟真实的鼠标事件序列 const eventConfig = { view: window, bubbles: true, cancelable: true, button: 0 }; ['mousedown', 'mouseup', 'click'].forEach(eventType => { button.dispatchEvent(new MouseEvent(eventType, eventConfig)); }); expandedCount++; if (i < 3) { console.log(`✓ 已触发第 ${i + 1} 个展开按钮`); } // 每10个按钮后稍微延迟一下,给浏览器处理时间 if (expandedCount % 10 === 0) { clickPromises.push(new Promise(resolve => setTimeout(resolve, 200))); } } catch (e) { console.error(`❌ 展开第 ${i + 1} 个商品失败:`, e); } } } console.log(`✓ 已触发 ${expandedCount} 个展开按钮(模拟点击事件)`); // 等待所有点击延迟完成 await Promise.all(clickPromises); // 等待API请求完成(优化为更短的等待时间) const waitTime = Math.max(1000, expandedCount * 80); // 最少1秒,每个商品80ms console.log(`⏳ 等待 ${waitTime}ms 让API请求完成...`); // 分段检查API请求数量(减少检查次数,更快响应) for (let i = 0; i < 2; i++) { await new Promise(resolve => setTimeout(resolve, waitTime / 2)); const currentApiCount = capturedApiData.length; const newCount = currentApiCount - beforeApiCount; if (newCount > 0) { console.log(` ✓ 已捕获 ${newCount} 个新API请求...`); } } // 检查是否捕获到新的API数据 const afterApiCount = capturedApiData.length; const newApiCount = afterApiCount - beforeApiCount; console.log(`📊 展开后共捕获 ${afterApiCount} 个API请求(新增 ${newApiCount} 个)`); if (newApiCount === 0) { console.log('⚠️ 警告:展开后未捕获到新的API请求!'); } else { console.log(`✓ 成功捕获到 ${newApiCount} 个新的API请求`); } return expandedCount; } // 更新面板数据 async function updatePanelData(autoExpand = true) { const statusEl = document.getElementById('dianxiaomi-status'); const contentEl = document.getElementById('dianxiaomi-content'); // 如果启用自动展开 if (autoExpand) { statusEl.textContent = '正在处理...'; statusEl.style.color = '#ff9800'; const expandedCount = await expandAllProducts(); if (expandedCount === 0) { statusEl.textContent = '未找到可展开的商品'; statusEl.style.color = '#ff4d4f'; return; } } statusEl.textContent = '正在获取数据...'; statusEl.style.color = '#1890ff'; setTimeout(() => { const productData = extractProductInfo(); displayProductData(productData); window.dianxiaomiProductData = productData; // 更新UI显示 if (productData.length === 0) { contentEl.innerHTML = '
⚠️ 未找到商品数据
请确保页面已加载完成
'; statusEl.textContent = '未找到数据'; statusEl.style.color = '#ff4d4f'; } else { const totalSpecs = productData.reduce((sum, p) => sum + p.specifications.length, 0); // 统计高价差商品数量 let highPriceRatioCount = 0; // 检查分类数据是否初始化 const categoryExists = CategoryManager.exists(); let html = `
✓ 成功获取 ${productData.length} 个商品,共 ${totalSpecs} 个规格
最新更新: ${new Date().toLocaleTimeString()} ${categoryExists ? '📁 分类数据: 已初始化' : '⚠️ 分类数据: 未初始化'}
${!categoryExists ? `
⚠️ 提示
需要手动移动一次分类来初始化分类数据,完成后即可使用批量移动功能。
` : ''} `; productData.forEach((item, index) => { // 计算最低价和最高价 let minPrice = Infinity; let maxPrice = -Infinity; item.specifications.forEach(spec => { const price = parseFloat(spec.price); if (!isNaN(price)) { minPrice = Math.min(minPrice, price); maxPrice = Math.max(maxPrice, price); } }); // 判断价格差是否超过5倍 const priceRatio = maxPrice / minPrice; const hasHighPriceRatio = priceRatio > 5; if (hasHighPriceRatio) highPriceRatioCount++; const warningStyle = hasHighPriceRatio ? 'border: 2px solid #ff4d4f; background: #fff1f0;' : ''; const warningBadge = hasHighPriceRatio ? `⚠️ 价差${priceRatio.toFixed(1)}倍` : ''; html += `
${index + 1}. ${item.name || '未知商品'} ${warningBadge}
📦 共 ${item.specifications.length} 个规格 | ID: ${item.idStr}
`; }); // 获取缓存信息 const cacheInfo = CacheManager.getInfo(); // 添加统计信息 html += `
📈 统计信息
商品数量: ${productData.length} 件
规格数量: ${totalSpecs} 个
API记录: ${capturedApiData.length} 条
${cacheInfo.exists ? `💾 缓存: ${cacheInfo.ageDays}天前 (${cacheInfo.count}条)` : '💾 缓存: 无'}
`; contentEl.innerHTML = html; statusEl.textContent = `已加载 ${productData.length} 个商品`; statusEl.style.color = '#52c41a'; // 控制移动分类按钮显示 const moveCategoryBtn = document.getElementById('dianxiaomi-move-category-btn'); if (moveCategoryBtn) { if (highPriceRatioCount > 0) { moveCategoryBtn.style.display = 'block'; moveCategoryBtn.textContent = `📁 移动分类 (${highPriceRatioCount})`; } else { moveCategoryBtn.style.display = 'none'; } } } }, 500); } // 编辑商品函数 - 打开店小秘编辑页面 window.editProduct = function(index) { const product = window.dianxiaomiProductData[index]; if (!product) { alert('商品数据不存在'); return; } // 使用 idStr 构建编辑链接 const editUrl = `https://www.dianxiaomi.com/web/shopeeSite/edit?id=${product.idStr}`; console.log('🔗 打开编辑页面:', editUrl); // 在新标签页打开编辑链接 window.open(editUrl, '_blank'); }; // 主函数 function main() { console.log('店小蜜价格助手已启动'); // 创建浮动面板 setTimeout(() => { createFloatingPanel(); console.log('✓ UI面板已创建'); // 不再自动执行数据获取,只保留手动获取功能 }, 1000); } // 页面加载完成后执行 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main); } else { main(); } })();