// ==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 += `
📦 共 ${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();
}
})();