您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
eCPM值显示两位小数
// ==UserScript== // @name 千川eCPM计算器 // @namespace http://tampermonkey.net/ // @version 5.0 // @description eCPM值显示两位小数 // @author You // @match https://qianchuan.jinritemai.com/* // @grant none // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; console.log('🚀 千川eCPM计算器启动...'); let currentUrl = window.location.href; let isAutoCalculating = false; let ecpmColumn = null; function createCalculatorPanel() { const panel = document.createElement('div'); panel.id = 'ecpm-calculator-panel'; panel.style.cssText = ` position: fixed; top: 20px; right: 20px; width: 450px; background: #fff; border: 2px solid #1890ff; border-radius: 8px; z-index: 999999; box-shadow: 0 4px 20px rgba(0,0,0,0.15); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; `; panel.innerHTML = ` <div id="panel-header" style=" background: linear-gradient(135deg, #1890ff, #40a9ff); color: white; padding: 12px 15px; border-radius: 6px 6px 0 0; cursor: move; display: flex; justify-content: space-between; align-items: center; user-select: none; white-space: nowrap; "> <span id="panel-title" style="font-weight: 600; font-size: 14px;">🚀 千川eCPM计算器</span> <div style="display: flex; gap: 5px;"> <button id="minimize-btn" style=" background: rgba(255,255,255,0.2); border: none; color: white; width: 24px; height: 24px; border-radius: 4px; cursor: pointer; font-size: 12px; ">−</button> <button id="close-btn" style=" background: rgba(255,255,255,0.2); border: none; color: white; width: 24px; height: 24px; border-radius: 4px; cursor: pointer; font-size: 12px; ">×</button> </div> </div> <div id="panel-content" style="padding: 15px;"> <div style="margin-bottom: 15px; padding: 12px; background: #f0f9ff; border-radius: 6px; border-left: 4px solid #1890ff;"> <div style="font-weight: 600; color: #1890ff; margin-bottom: 8px;">eCPM计算公式</div> <div style="font-size: 12px; color: #666; line-height: 1.4;"> • <strong>有出价时:</strong> 出价 × CTR × CVR × 1000<br> • <strong>无出价时:</strong> (客单价÷ROI) × CTR × CVR × 1000<br> • <strong>eCPM列显示在表格左侧,与表格行对齐</strong> </div> </div> <div style="margin-bottom: 15px; padding: 10px; background: #f6f8fa; border-radius: 6px;"> <label style="font-size: 12px; font-weight: 600; color: #333;">客单价设置:</label> <input id="unit-price" type="number" value="39.9" step="0.1" style=" width: 80px; margin-left: 8px; padding: 4px 8px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 12px; "> 元 </div> <div style="margin-bottom: 15px; padding: 10px; background: #fff7e6; border-radius: 6px; border: 1px solid #ffd666;"> <label style="display: flex; align-items: center; font-size: 12px; color: #333;"> <input id="auto-calculate" type="checkbox" checked style="margin-right: 8px;"> <span style="font-weight: 600;">页面切换时自动计算eCPM</span> </label> </div> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 15px;"> <button id="analyze-btn" style=" padding: 10px; background: #52c41a; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; ">📊 分析数据</button> <button id="calculate-btn" style=" padding: 10px; background: #1890ff; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; ">🚀 计算eCPM</button> </div> <button id="clear-btn" style=" width: 100%; padding: 8px; background: #fa8c16; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 12px; margin-bottom: 15px; ">🧹 清除结果</button> <div id="result-display" style=" background: #fafafa; border: 1px solid #e8e8e8; border-radius: 6px; padding: 12px; font-size: 11px; max-height: 250px; overflow-y: auto; white-space: pre-wrap; font-family: 'Consolas', 'Monaco', monospace; line-height: 1.4; ">点击"分析数据"开始...</div> </div> `; document.body.appendChild(panel); setupPanelEvents(); makePanelDraggable(); setupPageChangeListener(); } function setupPanelEvents() { document.getElementById('minimize-btn').onclick = (e) => { e.stopPropagation(); toggleMinimize(); }; document.getElementById('close-btn').onclick = (e) => { e.stopPropagation(); document.getElementById('ecpm-calculator-panel').remove(); if (ecpmColumn) { ecpmColumn.remove(); ecpmColumn = null; } }; document.getElementById('analyze-btn').onclick = analyzeTableData; document.getElementById('calculate-btn').onclick = calculateAllECPM; document.getElementById('clear-btn').onclick = clearAllResults; } function setupPageChangeListener() { // 监听URL变化 const observer = new MutationObserver(() => { if (window.location.href !== currentUrl) { currentUrl = window.location.href; onPageChange(); } }); observer.observe(document.body, { childList: true, subtree: true }); // 监听浏览器前进后退 window.addEventListener('popstate', onPageChange); // 监听点击事件(分页按钮等) document.addEventListener('click', (e) => { // 检查是否点击了分页相关的元素 const target = e.target; if (target.matches('.ovui-pagination *') || target.closest('.ovui-pagination') || target.textContent.includes('下一页') || target.textContent.includes('上一页') || /^\d+$/.test(target.textContent.trim())) { setTimeout(onPageChange, 1000); // 延迟1秒等待页面加载 } }); logResult('✅ 页面变化监听已启动'); } function onPageChange() { const autoCalculate = document.getElementById('auto-calculate'); if (autoCalculate && autoCalculate.checked && !isAutoCalculating) { isAutoCalculating = true; logResult('\n🔄 检测到页面变化,自动重新计算...'); // 清除旧结果 clearAllResults(); // 等待页面加载完成后自动计算 setTimeout(() => { analyzeTableData(); setTimeout(() => { calculateAllECPM(); isAutoCalculating = false; }, 1000); }, 2000); } } function toggleMinimize() { const content = document.getElementById('panel-content'); const panel = document.getElementById('ecpm-calculator-panel'); const btn = document.getElementById('minimize-btn'); const title = document.getElementById('panel-title'); if (content.style.display === 'none') { content.style.display = 'block'; panel.style.width = '450px'; btn.textContent = '−'; title.textContent = '🚀 千川eCPM计算器'; } else { content.style.display = 'none'; panel.style.width = '200px'; btn.textContent = '+'; title.textContent = '🚀 eCPM计算器'; } } function makePanelDraggable() { const panel = document.getElementById('ecpm-calculator-panel'); const header = document.getElementById('panel-header'); let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; header.onmousedown = dragMouseDown; function dragMouseDown(e) { e = e || window.event; e.preventDefault(); if (e.target.id === 'minimize-btn' || e.target.id === 'close-btn') { return; } pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } function elementDrag(e) { e = e || window.event; e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; let newTop = panel.offsetTop - pos2; let newLeft = panel.offsetLeft - pos1; newTop = Math.max(0, Math.min(newTop, window.innerHeight - panel.offsetHeight)); newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - panel.offsetWidth)); panel.style.top = newTop + "px"; panel.style.left = newLeft + "px"; panel.style.right = "auto"; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; } } function logResult(message) { const display = document.getElementById('result-display'); if (display) { display.textContent += message + '\n'; display.scrollTop = display.scrollHeight; } console.log(message); } function clearLog() { const display = document.getElementById('result-display'); if (display) display.textContent = ''; } function getTables() { const tables = document.querySelectorAll('table.ovui-table'); let headerTable = null; let dataTable = null; tables.forEach(table => { const hasHeaders = table.querySelectorAll('th').length > 0; const hasData = table.querySelectorAll('tbody tr').length > 0; if (hasHeaders && !hasData) { headerTable = table; } else if (hasData) { dataTable = table; } }); return { headerTable, dataTable }; } // 改进的行验证函数,跳过汇总行和表头扩展行 function isSummaryRow(row) { try { const cells = row.querySelectorAll('td'); // 检查是否为汇总行 for (let i = 0; i < cells.length; i++) { const cellText = cells[i].textContent.trim(); if (cellText.includes('共') && cellText.includes('条计划')) { return true; } } return false; } catch (error) { return false; } } // 新增函数:检查是否为表头扩展行或状态行 function isHeaderExtensionRow(row) { try { const cells = row.querySelectorAll('td'); // 如果行的单元格数量太少,可能是表头扩展行 if (cells.length < 5) { return true; } // 检查是否包含状态显示信息(如图片中的第二行) let hasStatusInfo = false; let hasActualData = false; for (let cell of cells) { const cellText = cell.textContent.trim(); const innerHTML = cell.innerHTML; // 检查是否包含状态相关的内容 if (cellText.includes('计划状态') || cellText.includes('投放状态') || innerHTML.includes('status') || cell.querySelector('.status')) { hasStatusInfo = true; } // 检查是否有实际的数据(数字、链接等) if (cellText.match(/^\d+/) || cell.querySelector('a[href]') || cellText.includes('ID:')) { hasActualData = true; break; } } // 如果只有状态信息而没有实际数据,认为是表头扩展行 return hasStatusInfo && !hasActualData; } catch (error) { return false; } } // 使用你提供的准确分析函数 function analyzeTableData() { clearLog(); logResult('📊 === 开始分析表格数据 ==='); const { headerTable, dataTable } = getTables(); if (!headerTable || !dataTable) { logResult('❌ 未找到完整的表格结构!'); return; } const headers = headerTable.querySelectorAll('th'); const headerTexts = Array.from(headers).map(h => h.textContent.trim()); const planNameColumnIndex = headerTexts.findIndex(text => text.includes('计划名称')); const bidColumnIndex = headerTexts.findIndex(text => text.includes('出价')); const roiColumnIndex = headerTexts.findIndex(text => text.includes('ROI目标')); const ctrColumnIndex = headerTexts.findIndex(text => text.includes('点击率')); const cvrColumnIndex = headerTexts.findIndex(text => text.includes('转化率')); logResult(`表头列位置:`); logResult(`- 计划名称列: ${planNameColumnIndex >= 0 ? planNameColumnIndex : '未找到'}`); logResult(`- 出价列: ${bidColumnIndex >= 0 ? bidColumnIndex : '未找到'}`); logResult(`- ROI目标列: ${roiColumnIndex >= 0 ? roiColumnIndex : '未找到'}`); logResult(`- 点击率列: ${ctrColumnIndex >= 0 ? ctrColumnIndex : '未找到'}`); logResult(`- 转化率列: ${cvrColumnIndex >= 0 ? cvrColumnIndex : '未找到'}`); const rows = dataTable.querySelectorAll('tbody tr'); logResult(`\n找到 ${rows.length} 行数据`); let validRows = 0; let skippedRows = 0; rows.forEach((row, index) => { // 检查是否为汇总行 if (isSummaryRow(row)) { const cells = row.querySelectorAll('td'); const firstCellText = cells.length > 0 ? cells[0].textContent.trim() : ''; logResult(`第${index + 1}行: ⏭️ 跳过汇总行 - "${firstCellText.substring(0, 30)}..."`); skippedRows++; return; } // 检查是否为表头扩展行 if (isHeaderExtensionRow(row)) { logResult(`第${index + 1}行: ⏭️ 跳过表头扩展行/状态行`); skippedRows++; return; } const cells = row.querySelectorAll('td'); let bid = 0, roi = 0, ctr = 0, cvr = 0; if (bidColumnIndex >= 0 && cells[bidColumnIndex]) { const bidText = cells[bidColumnIndex].textContent.trim(); if (bidText !== '-' && bidText.match(/^\d+\.?\d*$/)) { bid = parseFloat(bidText); } } if (roiColumnIndex >= 0 && cells[roiColumnIndex]) { const roiText = cells[roiColumnIndex].textContent.trim(); if (roiText.includes('日总支付ROI')) { const match = roiText.match(/(\d+\.?\d*)/); if (match) roi = parseFloat(match[1]); } } if (ctrColumnIndex >= 0 && cells[ctrColumnIndex]) { const ctrText = cells[ctrColumnIndex].textContent.trim(); if (ctrText.includes('%')) { const ctrValue = parseFloat(ctrText.replace(/[^0-9.-]/g, '')); if (ctrValue > 0) ctr = ctrValue; } } if (cvrColumnIndex >= 0 && cells[cvrColumnIndex]) { const cvrText = cells[cvrColumnIndex].textContent.trim(); if (cvrText.includes('%')) { const cvrValue = parseFloat(cvrText.replace(/[^0-9.-]/g, '')); if (cvrValue > 0) cvr = cvrValue; } } const canCalculate = (bid > 0 || roi > 0) && ctr > 0 && cvr > 0; if (canCalculate) { validRows++; const formula = bid > 0 ? '出价公式' : 'ROI公式'; logResult(`第${index + 1}行: ✅ 可计算 (${formula}) - 出价:${bid || '无'}, ROI:${roi || '无'}, CTR:${ctr}%, CVR:${cvr}%`); } else { logResult(`第${index + 1}行: ❌ 不可计算 - 出价:${bid || '无'}, ROI:${roi || '无'}, CTR:${ctr}%, CVR:${cvr}%`); } }); logResult(`\n📊 分析结果:`); logResult(`- 总行数: ${rows.length}`); logResult(`- 跳过行数: ${skippedRows} (汇总行+表头扩展行)`); logResult(`- 有效数据行: ${rows.length - skippedRows}`); logResult(`- 可计算行数: ${validRows}`); logResult(`- 无效行数: ${rows.length - skippedRows - validRows}`); logResult('\n📊 === 分析完成 ==='); } function calculateAllECPM() { clearLog(); logResult('🚀 === 开始计算eCPM ==='); const unitPrice = parseFloat(document.getElementById('unit-price').value) || 39.9; logResult(`客单价设置: ${unitPrice}元`); const { headerTable, dataTable } = getTables(); if (!headerTable || !dataTable) { logResult('❌ 未找到表格结构!'); if (!isAutoCalculating) { alert('❌ 未找到表格数据,请刷新页面后重试!'); } return; } // 先清除所有旧的eCPM列 clearAllResults(); // 获取列索引 const headers = headerTable.querySelectorAll('th'); const headerTexts = Array.from(headers).map(h => h.textContent.trim()); const bidColumnIndex = headerTexts.findIndex(text => text.includes('出价')); const roiColumnIndex = headerTexts.findIndex(text => text.includes('ROI目标')); const ctrColumnIndex = headerTexts.findIndex(text => text.includes('点击率')); const cvrColumnIndex = headerTexts.findIndex(text => text.includes('转化率')); const rows = dataTable.querySelectorAll('tbody tr'); let successCount = 0; let skippedCount = 0; let ecpmValues = []; rows.forEach((row, index) => { try { // 检查是否为汇总行 if (isSummaryRow(row)) { logResult(`第${index + 1}行: ⏭️ 跳过汇总行`); skippedCount++; ecpmValues.push({ value: 0, text: '-', title: '汇总行' }); return; } // 检查是否为表头扩展行 if (isHeaderExtensionRow(row)) { logResult(`第${index + 1}行: ⏭️ 跳过表头扩展行/状态行`); skippedCount++; ecpmValues.push({ value: 0, text: '-', title: '状态行' }); return; } const cells = row.querySelectorAll('td'); let bid = 0, roi = 0, ctr = 0, cvr = 0; // 使用原始的列索引 if (bidColumnIndex >= 0 && cells[bidColumnIndex]) { const bidText = cells[bidColumnIndex].textContent.trim(); if (bidText !== '-' && bidText.match(/^\d+\.?\d*$/)) { bid = parseFloat(bidText); } } if (roiColumnIndex >= 0 && cells[roiColumnIndex]) { const roiText = cells[roiColumnIndex].textContent.trim(); if (roiText.includes('日总支付ROI')) { const match = roiText.match(/(\d+\.?\d*)/); if (match) roi = parseFloat(match[1]); } } if (ctrColumnIndex >= 0 && cells[ctrColumnIndex]) { const ctrText = cells[ctrColumnIndex].textContent.trim(); if (ctrText.includes('%')) { const ctrValue = parseFloat(ctrText.replace(/[^0-9.-]/g, '')); if (ctrValue > 0) ctr = ctrValue; } } if (cvrColumnIndex >= 0 && cells[cvrColumnIndex]) { const cvrText = cells[cvrColumnIndex].textContent.trim(); if (cvrText.includes('%')) { const cvrValue = parseFloat(cvrText.replace(/[^0-9.-]/g, '')); if (cvrValue > 0) cvr = cvrValue; } } // 计算eCPM let ecpm = 0; let formula = ''; let calculation = ''; if ((bid > 0 || roi > 0) && ctr > 0 && cvr > 0) { const ctrDecimal = ctr / 100; const cvrDecimal = cvr / 100; if (bid > 0) { ecpm = bid * ctrDecimal * cvrDecimal * 1000; formula = '出价公式'; calculation = `${bid} × ${ctrDecimal} × ${cvrDecimal} × 1000 = ${ecpm.toFixed(2)}`; } else if (roi > 0) { ecpm = (unitPrice / roi) * ctrDecimal * cvrDecimal * 1000; formula = 'ROI公式'; calculation = `(${unitPrice} ÷ ${roi}) × ${ctrDecimal} × ${cvrDecimal} × 1000 = ${ecpm.toFixed(2)}`; } logResult(`第${index + 1}行: ${calculation}`); successCount++; ecpmValues.push({ value: ecpm, text: ecpm.toFixed(2), // 修改:显示两位小数 title: `eCPM: ${ecpm.toFixed(2)} (${formula})` }); } else { ecpmValues.push({ value: 0, text: '-', title: '无法计算eCPM' }); } } catch (error) { logResult(`第${index + 1}行处理出错: ${error.message}`); console.error('处理行数据出错:', error); ecpmValues.push({ value: 0, text: '错误', title: '计算出错' }); } }); // 创建浮动的eCPM列,传入计算好的数据 createFloatingECPMColumn(headerTable, dataTable, ecpmValues); logResult(`\n🚀 === 计算完成 ===`); logResult(`跳过行数: ${skippedCount} (汇总行+表头扩展行)`); logResult(`成功计算: ${successCount} 行`); if (successCount > 0) { if (!isAutoCalculating) { alert(`🎉 成功计算了 ${successCount} 行数据的eCPM!\n跳过了 ${skippedCount} 个无效行\neCPM列显示在表格左侧。`); } } else { if (!isAutoCalculating) { alert(`😅 没有找到可计算的数据行!`); } } } function createFloatingECPMColumn(headerTable, dataTable, ecpmValues) { if (ecpmColumn) { ecpmColumn.remove(); } // 获取表头和数据表格的位置 const headerRect = headerTable.getBoundingClientRect(); const dataRect = dataTable.getBoundingClientRect(); // 创建eCPM列容器,稍微增加宽度以容纳两位小数 ecpmColumn = document.createElement('div'); ecpmColumn.id = 'floating-ecpm-column'; ecpmColumn.style.cssText = ` position: fixed; left: ${Math.min(headerRect.left, dataRect.left) - 60}px; top: ${headerRect.top}px; width: 55px; background: white; border: 1px solid #d9d9d9; border-radius: 4px; z-index: 1000; box-shadow: -2px 0 8px rgba(0,0,0,0.1); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; `; // 添加表头 const header = document.createElement('div'); header.style.cssText = ` background: linear-gradient(135deg, #52c41a, #73d13d); color: white; text-align: center; font-weight: bold; padding: 8px 4px; font-size: 12px; border-bottom: 1px solid #389e0d; height: ${headerRect.height - 2}px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; `; header.textContent = 'eCPM'; ecpmColumn.appendChild(header); // 添加数据行,与表格行高度对齐 const dataRows = dataTable.querySelectorAll('tbody tr'); ecpmValues.forEach((item, index) => { const cell = document.createElement('div'); cell.className = 'ecpm-data-cell'; // 获取对应表格行的高度 let rowHeight = 40; // 默认高度 if (dataRows[index]) { rowHeight = dataRows[index].offsetHeight; } cell.style.cssText = ` padding: 4px 2px; text-align: center; font-size: 10px; border-bottom: 1px solid #f0f0f0; height: ${rowHeight}px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; ${item.value > 0 ? 'background: linear-gradient(135deg, #52c41a, #73d13d); color: white; font-weight: bold;' : 'background: #f5f5f5; color: #999;' } `; cell.textContent = item.text; cell.title = item.title; ecpmColumn.appendChild(cell); }); document.body.appendChild(ecpmColumn); // 监听滚动和窗口变化,保持位置同步 function updatePosition() { const newHeaderRect = headerTable.getBoundingClientRect(); const newDataRect = dataTable.getBoundingClientRect(); ecpmColumn.style.left = `${Math.min(newHeaderRect.left, newDataRect.left) - 60}px`; ecpmColumn.style.top = `${newHeaderRect.top}px`; } // 监听多种滚动容器 const scrollContainers = [ window, document.querySelector('.ovui-table-wrapper'), document.querySelector('.table-container'), document.querySelector('.ovui-table-scroll'), dataTable.closest('.ovui-scrollbar') ].filter(Boolean); scrollContainers.forEach(container => { container.addEventListener('scroll', updatePosition, { passive: true }); }); window.addEventListener('resize', updatePosition); logResult('✅ eCPM浮动列已创建并定位到表格左侧'); } function clearAllResults() { if (ecpmColumn) { ecpmColumn.remove(); ecpmColumn = null; } clearLog(); logResult('🧹 已清除所有eCPM列'); } // 启动 setTimeout(() => { createCalculatorPanel(); }, 1000); })();