1072 lines
37 KiB
JavaScript
1072 lines
37 KiB
JavaScript
// ==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 = `
|
||
<div class="koc-window-header">
|
||
<span class="koc-window-title">KOC数据采集</span>
|
||
<button class="koc-close-btn" title="关闭">×</button>
|
||
</div>
|
||
<div class="koc-window-content">
|
||
<div class="koc-api-status">
|
||
<div class="koc-status-indicator"></div>
|
||
<span class="koc-status-text">准备采集...</span>
|
||
</div>
|
||
<div class="koc-actions">
|
||
<button class="koc-download-btn" type="button">
|
||
<svg class="koc-btn-icon" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
|
||
</svg>
|
||
<span class="koc-btn-text">开始采集</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 设置悬浮窗样式
|
||
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 = `
|
||
<svg class="koc-btn-icon" viewBox="0 0 24 24" fill="currentColor" style="animation: spin 1s linear infinite;">
|
||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||
</svg>
|
||
<span class="koc-btn-text">采集中...</span>
|
||
`;
|
||
|
||
// 从当前页面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 = `
|
||
<svg class="koc-btn-icon" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
|
||
</svg>
|
||
<span class="koc-btn-text">开始采集</span>
|
||
`;
|
||
}
|
||
|
||
// 恢复状态显示
|
||
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 = `
|
||
<div class="koc-toast-icon">
|
||
${getToastIcon(type)}
|
||
</div>
|
||
<div class="koc-toast-message">${message}</div>
|
||
`;
|
||
|
||
// 添加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 `<svg viewBox="0 0 24 24" fill="currentColor" style="color: #10b981;">
|
||
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||
</svg>`;
|
||
case 'error':
|
||
return `<svg viewBox="0 0 24 24" fill="currentColor" style="color: #ef4444;">
|
||
<path d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||
</svg>`;
|
||
case 'warning':
|
||
return `<svg viewBox="0 0 24 24" fill="currentColor" style="color: #f59e0b;">
|
||
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"/>
|
||
</svg>`;
|
||
default:
|
||
return `<svg viewBox="0 0 24 24" fill="currentColor" style="color: #3b82f6;">
|
||
<path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||
</svg>`;
|
||
}
|
||
}
|
||
|
||
// 页面加载完成后运行
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|