// ==UserScript== // @name 蝉妈妈产品历史KOC助手 // @namespace https://github.com/scriptCat/ // @version 1.0.0 // @description 蝉妈妈平台产品历史KOC数据分析和导出助手 // @author wangxi // @match https://www.chanmama.com/promotionRank/* // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_setValue // @grant GM_getValue // @grant GM_log // @grant GM_addStyle // @grant GM_setClipboard // @connect chanmama.com // @connect api-service.chanmama.com // @require https://cdn.jsdelivr.net/npm/sweetalert2@11 // @license MIT // ==/UserScript== /* ==UserConfig== settings: enabled: title: 启用功能 description: 是否启用蝉妈妈产品历史KOC助手功能 type: checkbox default: true timeout: title: 超时时间 description: 网络请求超时时间 type: number default: 30 min: 10 max: 120 unit: 秒 debug: title: 调试模式 description: 启用详细日志输出 type: checkbox default: false ==/UserConfig== */ (function() { 'use strict'; // 配置常量 const CONFIG = { debug: true, // 强制开启调试模式 enabled: GM_getValue('settings.enabled', true), timeout: GM_getValue('settings.timeout', 30) * 1000, api: { baseUrl: 'https://api-service.chanmama.com', endpoint: '/v1/product/author/analysis' }, pagination: { pageSize: 30, amountThreshold: 10000 // 金额门槛,低于此值停止翻页 } }; // 工具函数 function log(message, level = 'info') { const logMessage = `[${GM_info.script.name}] ${message}`; // 强制输出所有日志到GM日志和控制台,便于调试 GM_log(logMessage, level); console.log(logMessage); } function showNotification(title, text, type = 'info') { GM_notification({ title: title, text: text, timeout: 5000, onclick: () => log('通知被点击') }); } function makeRequest(url, options = {}) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: options.method || 'GET', url: url, headers: { 'Content-Type': 'application/json', ...options.headers }, data: options.data, timeout: CONFIG.timeout, anonymous: false, onload: (response) => { if (response.status >= 200 && response.status < 300) { try { const data = JSON.parse(response.responseText); resolve(data); } catch (error) { resolve(response.responseText); } } else { reject(new Error(`HTTP ${response.status}: ${response.statusText}`)); } }, onerror: () => reject(new Error('网络请求失败')), ontimeout: () => reject(new Error('请求超时')) }); }); } // SweetAlert2 弹窗函数 function showInfo(title, text) { Swal.fire({ title: title, text: text, icon: 'info', confirmButtonText: '确定' }); } function showSuccess(title, text) { Swal.fire({ title: title, text: text, icon: 'success', timer: 3000, showConfirmButton: false }); } function showError(title, text) { Swal.fire({ title: title, text: text, icon: 'error', confirmButtonText: '确定' }); } async function showConfirm(title, text) { const result = await Swal.fire({ title: title, text: text, icon: 'question', showCancelButton: true, confirmButtonText: '确定', cancelButtonText: '取消' }); return result.isConfirmed; } // 数据导出函数 function exportToJSON(data, filename = 'koc_history') { // JSON导出逻辑 log('导出JSON格式数据'); const jsonStr = JSON.stringify(data, null, 2); GM_setClipboard(jsonStr); showSuccess('导出成功', 'JSON数据已复制到剪贴板'); } // KOC数据处理函数 async function fetchKOCHistory(productId) { try { log(`获取产品 ${productId} 的KOC历史数据`); const response = await makeRequest(`${CONFIG.api.baseUrl}${CONFIG.api.endpoint}`, { method: 'POST', data: JSON.stringify({ productId }) }); return response; } catch (error) { log(`获取KOC历史数据失败: ${error.message}`, 'error'); throw error; } } /** * 解析区间文本为平均值 * 例如: "750~1000" -> 875, "1w~2.5w" -> 17500 */ function parseRangeText(rangeText) { if (!rangeText || rangeText === '0' || rangeText === '') return 0; // 正确处理万单位:先解析每个数值,再转换单位 const split = rangeText.split('~'); const values = split.map(s => { const trimmed = s.trim(); if (trimmed.includes('w')) { // 提取数字部分,乘以10000 const numPart = parseFloat(trimmed.replace('w', '')) || 0; return numPart * 10000; } else { return parseFloat(trimmed) || 0; } }); const result = values.reduce((sum, val) => sum + val, 0) / values.length; return result; } /** * 解析KOC数据 */ function parseKOCData(apiResponse) { if (!apiResponse || !apiResponse.data || !apiResponse.data.list) { log('数据格式错误', 'error'); return []; } const results = []; const kocList = apiResponse.data.list; kocList.forEach((koc, index) => { // 基础信息 const baseInfo = { 昵称: koc.nickname || '', 达人UID: koc.unique_id || koc.short_id || '', 粉丝数: koc.follower_count || 0, 类型: koc.label || '', 口碑分: (koc.reputation && koc.reputation.score) || 0, 关联商品销售量区间: koc.volume_text || '', 关联商品销售金额区间: koc.amount_text || '', 预估关联商品销售量: parseRangeText(koc.volume_text), 预估关联商品销售金额: parseRangeText(koc.amount_text) }; // 计算关联商品视频的总销售量和金额 let totalVideoVolume = 0; let totalVideoAmount = 0; const videoProductIds = new Set(); if (koc.a && Array.isArray(koc.a)) { koc.a.forEach((video, videoIndex) => { if (video.product_id) { videoProductIds.add(video.product_id); } const volumeValue = parseRangeText(video.volume_text); const amountValue = parseRangeText(video.amount_text); totalVideoVolume += volumeValue; totalVideoAmount += amountValue; }); } baseInfo.预估关联商品视频销售量 = totalVideoVolume; baseInfo.预估关联商品视频销售金额 = totalVideoAmount; baseInfo.关联商品蝉妈妈ID = Array.from(videoProductIds).join(','); results.push(baseInfo); }); return results; } // UI相关函数 - 创建简洁美观的悬浮窗 function createFloatingWindow() { // 检查悬浮窗是否已存在 if (document.querySelector('.koc-floating-window')) { log('悬浮窗已存在'); return true; } // 创建简洁的悬浮窗 const floatingWindow = document.createElement('div'); floatingWindow.className = 'koc-floating-window'; floatingWindow.innerHTML = `
KOC数据采集
准备采集...
`; // 设置悬浮窗样式 floatingWindow.style.cssText = ` position: fixed; bottom: 30px; right: 30px; width: 180px; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); z-index: 9999; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; user-select: none; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); opacity: 0; transform: translateY(20px) scale(0.95); `; // 添加美观的样式 GM_addStyle(` .koc-floating-window { animation: kocSlideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; } @keyframes kocSlideIn { to { opacity: 1; transform: translateY(0) scale(1); } } .koc-window-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px 8px; border-bottom: 1px solid rgba(0, 0, 0, 0.06); cursor: move; user-select: none; } .koc-window-title { font-size: 13px; font-weight: 600; color: #374151; letter-spacing: 0.025em; } .koc-close-btn { width: 20px; height: 20px; border: none; background: transparent; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px; color: #6b7280; transition: all 0.2s ease; font-weight: 300; } .koc-close-btn:hover { background: rgba(239, 68, 68, 0.1); color: #ef4444; transform: scale(1.1); } .koc-window-content { padding: 16px; } .koc-api-status { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: rgba(16, 185, 129, 0.05); border-radius: 6px; border: 1px solid rgba(16, 185, 129, 0.1); margin-bottom: 12px; } .koc-actions { display: flex; flex-direction: column; gap: 8px; } .koc-download-btn { width: 100%; padding: 12px 16px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 500; display: flex; align-items: center; justify-content: center; gap: 8px; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); position: relative; overflow: hidden; } .koc-download-btn::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); transition: left 0.5s; } .koc-download-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); } .koc-download-btn:hover::before { left: 100%; } .koc-download-btn:active { transform: translateY(0); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); } .koc-download-btn:disabled { background: linear-gradient(135deg, #d1d5db 0%, #9ca3af 100%); cursor: not-allowed; transform: none; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .koc-btn-icon { width: 16px; height: 16px; flex-shrink: 0; } .koc-status-indicator { width: 8px; height: 8px; border-radius: 50%; background: #10b981; animation: kocPulse 2s infinite; } @keyframes kocPulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.5; transform: scale(1.2); } } .koc-status-text { font-size: 11px; color: #6b7280; font-weight: 500; } .koc-floating-window.dragging { transition: none !important; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15); transform: scale(1.02); } /* 状态颜色 */ .koc-status.waiting .koc-status-indicator { background: #10b981; } .koc-status.processing .koc-status-indicator { background: #f59e0b; animation: kocSpin 1s linear infinite; } .koc-status.success .koc-status-indicator { background: #10b981; } .koc-status.error .koc-status-indicator { background: #ef4444; } @keyframes kocSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } /* 响应式设计 */ @media (max-width: 768px) { .koc-floating-window { bottom: 20px; right: 20px; width: 160px; } } `); // 添加到页面 document.body.appendChild(floatingWindow); // 设置拖拽功能 setupSimpleDrag(floatingWindow); // 设置事件监听器 setupFloatingWindowEvents(floatingWindow); // 显示动画 setTimeout(() => { floatingWindow.style.opacity = '1'; floatingWindow.style.transform = 'translateY(0) scale(1)'; }, 100); return true; } // 更新悬浮窗状态 function updateFloatingWindowStatus(message, type = 'info') { const floatingWindow = document.querySelector('.koc-floating-window'); if (!floatingWindow) return; const statusText = floatingWindow.querySelector('.koc-status-text'); const statusIndicator = floatingWindow.querySelector('.koc-status-indicator'); if (statusText) { statusText.textContent = message; } if (statusIndicator) { statusIndicator.className = 'koc-status-indicator'; statusIndicator.classList.add(`koc-status-${type}`); } } // 处理下载按钮点击 async function handleDownloadClick() { const floatingWindow = document.querySelector('.koc-floating-window'); if (!floatingWindow) { log('悬浮窗不存在', 'error'); return; } const downloadBtn = floatingWindow.querySelector('.koc-download-btn'); if (!downloadBtn) { log('下载按钮不存在', 'error'); return; } if (downloadBtn.disabled) { log('按钮已禁用,请等待当前操作完成', 'warn'); return; } try { log('=== 开始KOC数据采集 ===', 'info'); // 设置按钮状态为处理中 setStatus(floatingWindow, 'processing', '正在采集数据...'); downloadBtn.disabled = true; downloadBtn.innerHTML = ` 采集中... `; // 从当前页面URL提取product_id function extractProductIdFromUrl() { const url = window.location.href; const match = url.match(/promotionRank\/([^\.]+)\.html/); return match ? match[1] : "MUJ0VtooAuIRKA33vMWQN2QUo4A016WL"; } // 计算时间范围(近365天,T-1结束) function getDateRange() { const endDate = new Date(); endDate.setDate(endDate.getDate() - 1); // T-1 const startDate = new Date(); startDate.setDate(startDate.getDate() - 365); // 近365天 return { start_date: startDate.toISOString().split('T')[0], end_date: endDate.toISOString().split('T')[0] }; } const dateRange = getDateRange(); const productId = extractProductIdFromUrl(); const apiUrl = `${CONFIG.api.baseUrl}${CONFIG.api.endpoint}`; log(`产品ID: ${productId}`, 'debug'); log(`时间范围: ${dateRange.start_date} 到 ${dateRange.end_date}`, 'debug'); let allData = []; let currentPage = 1; let shouldContinue = true; while (shouldContinue) { log(`请求第 ${currentPage} 页数据`, 'info'); // 构造请求参数 const requestParams = new URLSearchParams({ product_id: productId, sort: 'amount', order_by: 'desc', has_contact: '0', is_new_corp: '0', self_play: '0', shop_play: '0', author_play: '1', live_room_status: '0', category: '', reputation_level: '-1', follower_count: '', keyword: '', page: currentPage.toString(), size: CONFIG.pagination.pageSize.toString(), take_product_method: '1', start_date: dateRange.start_date, end_date: dateRange.end_date }).toString(); log(`请求参数: ${requestParams}`, 'debug'); // 发起API请求 const response = await makeRequest(`${apiUrl}?${requestParams}`, { method: 'GET' }); // 解析数据 const parsedData = parseKOCData(response); if (parsedData.length === 0) { log(`第 ${currentPage} 页无数据,停止采集`, 'info'); break; } // 检查金额门槛 - 使用预估关联商品视频销售金额的最小值 const amountValues = parsedData.map(author => author.预估关联商品视频销售金额 || 0); const minAmount = Math.min(...amountValues); log(`第 ${currentPage} 页最小金额: ${minAmount}`, 'debug'); if (minAmount < CONFIG.pagination.amountThreshold) { log(`达到金额门槛 ${CONFIG.pagination.amountThreshold},停止采集`, 'info'); // 过滤掉低于门槛的数据 const filteredData = parsedData.filter(author => author.预估关联商品视频销售金额 >= CONFIG.pagination.amountThreshold); allData = allData.concat(filteredData); shouldContinue = false; } else { allData = allData.concat(parsedData); currentPage++; // 防止无限循环,最多采集100页 if (currentPage > 100) { log('达到最大页数限制,停止采集', 'warn'); shouldContinue = false; } } // 更新状态 setStatus(floatingWindow, 'processing', `已采集 ${allData.length} 条数据`); // 延迟1秒,避免请求过于频繁 await new Promise(resolve => setTimeout(resolve, 1000)); } log(`=== 数据采集完成 ===`, 'info'); log(`总共采集 ${allData.length} 条符合条件的达人数据`, 'info'); // 打印完整数据到日志 log('📊 KOC数据采集结果:', 'info'); log(`📈 共采集 ${allData.length} 条达人数据`, 'info'); log('📋 数据详情:', 'info'); log(JSON.stringify(allData, null, 2), 'info'); // 显示成功提示 setStatus(floatingWindow, 'success', `已采集 ${allData.length} 条数据`); showSuccess('采集成功', `已采集 ${allData.length} 条符合条件的达人数据,请查看控制台`); // 5秒后恢复按钮状态 setTimeout(() => { resetButton(floatingWindow); }, 5000); } catch (error) { log(`=== 数据采集失败 ===`, 'error'); log(`错误详情: ${error.message}`, 'error'); log(`错误堆栈: ${error.stack}`, 'error'); setStatus(floatingWindow, 'error', '采集失败'); showError('采集失败', error.message); // 5秒后恢复按钮状态 setTimeout(() => { resetButton(floatingWindow); }, 5000); } } function getCurrentProductId() { // 从页面URL或DOM元素中获取产品ID const urlMatch = window.location.pathname.match(/\/product\/(\d+)/); if (urlMatch) { return urlMatch[1]; } // 尝试从页面元素获取 const productElement = document.querySelector('[data-product-id]'); if (productElement) { return productElement.getAttribute('data-product-id'); } return null; } // 主逻辑 async function init() { if (!CONFIG.enabled) { log('脚本已禁用'); return; } try { // 输出启动信息 log('🎯 KOC数据采集工具启动', 'info'); // 等待页面加载完成 await waitForPageLoad(); // 检查是否在正确的页面 if (!isValidPage()) { log('当前页面不支持此功能'); return; } // 创建悬浮窗 let windowCreated = createFloatingWindow(); if (!windowCreated) { log('首次创建悬浮窗失败,延迟重试...', 'warn'); setTimeout(() => { windowCreated = createFloatingWindow(); if (!windowCreated) { log('延迟重试失败,设置DOM观察器', 'warn'); setupWindowObserver(); } }, 2000); } showToast('KOC数据采集工具已就绪', 'success'); } catch (error) { log(`初始化失败: ${error.message}`, 'error'); showToast('初始化失败', 'error'); } } function setupWindowObserver() { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList') { // 检查是否已经有悬浮窗 if (document.querySelector('.koc-floating-window')) { observer.disconnect(); return; } // 简单创建悬浮窗 createFloatingWindow(); observer.disconnect(); } }); }); observer.observe(document.body, { childList: true, subtree: true }); log('DOM观察器已设置'); } function waitForPageLoad() { return new Promise((resolve) => { if (document.readyState === 'complete') { resolve(); } else { window.addEventListener('load', resolve); } }); } function isValidPage() { // 检查是否在蝉妈妈的推广排名页面 return window.location.hostname.includes('chanmama.com') && window.location.pathname.includes('/promotionRank'); } // 备用拖拽方案 function setupSimpleDrag(floatingWindow) { const header = floatingWindow.querySelector('.koc-window-header'); let isDragging = false; let startX, startY, startLeft, startTop; header.addEventListener('mousedown', (e) => { if (e.target.closest('.koc-close-btn')) return; isDragging = true; startX = e.clientX; startY = e.clientY; // 获取当前位置,正确处理bottom/right定位 const rect = floatingWindow.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; floatingWindow.classList.add('dragging'); e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; let newLeft = startLeft + deltaX; let newTop = startTop + deltaY; // 限制在视窗内 const maxX = window.innerWidth - floatingWindow.offsetWidth; const maxY = window.innerHeight - floatingWindow.offsetHeight; newLeft = Math.max(0, Math.min(newLeft, maxX)); newTop = Math.max(0, Math.min(newTop, maxY)); // 设置新位置,清除bottom/right定位 floatingWindow.style.left = newLeft + 'px'; floatingWindow.style.top = newTop + 'px'; floatingWindow.style.right = 'auto'; floatingWindow.style.bottom = 'auto'; }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; floatingWindow.classList.remove('dragging'); } }); } // 设置悬浮窗事件 function setupFloatingWindowEvents(floatingWindow) { const downloadBtn = floatingWindow.querySelector('.koc-download-btn'); const closeBtn = floatingWindow.querySelector('.koc-close-btn'); // 关闭功能 closeBtn.addEventListener('click', () => { floatingWindow.style.opacity = '0'; floatingWindow.style.transform = 'translateY(20px) scale(0.95)'; setTimeout(() => { floatingWindow.remove(); }, 300); }); // 下载按钮点击 - 直接调用handleDownloadClick downloadBtn.addEventListener('click', handleDownloadClick); } // 设置状态 function setStatus(floatingWindow, status, text) { const apiStatusContainer = floatingWindow.querySelector('.koc-api-status'); const apiStatusText = floatingWindow.querySelector('.koc-api-status .koc-status-text'); if (apiStatusContainer && apiStatusText) { // 移除所有状态类 apiStatusContainer.classList.remove('waiting', 'processing', 'success', 'error'); // 添加新状态类 apiStatusContainer.classList.add(status); apiStatusText.textContent = text; // 根据状态更新样式 switch (status) { case 'processing': apiStatusContainer.style.background = 'rgba(245, 158, 11, 0.1)'; apiStatusContainer.style.borderColor = 'rgba(245, 158, 11, 0.2)'; break; case 'success': apiStatusContainer.style.background = 'rgba(16, 185, 129, 0.1)'; apiStatusContainer.style.borderColor = 'rgba(16, 185, 129, 0.2)'; break; case 'error': apiStatusContainer.style.background = 'rgba(239, 68, 68, 0.1)'; apiStatusContainer.style.borderColor = 'rgba(239, 68, 68, 0.2)'; break; default: apiStatusContainer.style.background = 'rgba(16, 185, 129, 0.05)'; apiStatusContainer.style.borderColor = 'rgba(16, 185, 129, 0.1)'; } } } // 重置按钮 function resetButton(floatingWindow) { const downloadBtn = floatingWindow.querySelector('.koc-download-btn'); if (downloadBtn) { downloadBtn.disabled = false; downloadBtn.innerHTML = ` 开始采集 `; } // 恢复状态显示 const apiStatusText = floatingWindow.querySelector('.koc-api-status .koc-status-text'); if (apiStatusText) { apiStatusText.textContent = '准备采集...'; } setStatus(floatingWindow, 'waiting', '准备采集...'); } // 创建简洁的toast提示 function showToast(message, type = 'info') { // 移除已存在的toast const existingToast = document.querySelector('.koc-toast'); if (existingToast) { existingToast.remove(); } const toast = document.createElement('div'); toast.className = `koc-toast koc-toast-${type}`; toast.innerHTML = `
${getToastIcon(type)}
${message}
`; // 添加toast样式 GM_addStyle(` .koc-toast { position: fixed; top: 20px; right: 20px; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 12px; padding: 16px 20px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); z-index: 10000; display: flex; align-items: center; gap: 12px; font-size: 15px; font-weight: 600; max-width: 350px; min-width: 280px; animation: kocToastSlideIn 0.3s ease; transition: all 0.3s ease; } @keyframes kocToastSlideIn { from { opacity: 0; transform: translateX(100%); } to { opacity: 1; transform: translateX(0); } } .koc-toast-icon { width: 20px; height: 20px; flex-shrink: 0; } .koc-toast-message { color: #374151; line-height: 1.4; } .koc-toast-success { border-left: 5px solid #10b981; } .koc-toast-error { border-left: 5px solid #ef4444; } .koc-toast-info { border-left: 5px solid #3b82f6; } .koc-toast-warning { border-left: 5px solid #f59e0b; } .koc-toast.fade-out { opacity: 0; transform: translateX(100%); } `); document.body.appendChild(toast); // 3秒后自动消失 setTimeout(() => { toast.classList.add('fade-out'); setTimeout(() => { if (toast.parentNode) { toast.remove(); } }, 300); }, 3000); // 点击关闭 toast.addEventListener('click', () => { toast.classList.add('fade-out'); setTimeout(() => { if (toast.parentNode) { toast.remove(); } }, 300); }); } // 获取toast图标 function getToastIcon(type) { switch (type) { case 'success': return ` `; case 'error': return ` `; case 'warning': return ` `; default: return ` `; } } // 页面加载完成后运行 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();