您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
支持8种不同角度的GMV计算,现代化苹果风格UI,数据记录功能
// ==UserScript== // @name GMV计算器 - 多角度业务分析工具 // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description 支持8种不同角度的GMV计算,现代化苹果风格UI,数据记录功能 // @author Your Name // @match *://*/* // @grant GM_setValue // @license MIT // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // ==/UserScript== (function() { 'use strict'; // 样式定义 const styles = ` .gmv-calculator { position: fixed; top: 20px; right: 20px; width: 420px; max-height: 90vh; background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(20px); border-radius: 20px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.2); transition: all 0.3s ease; } .gmv-calculator.minimized { width: 60px; height: 60px; border-radius: 30px; } .gmv-header { background: linear-gradient(135deg, #007AFF 0%, #5856D6 100%); color: white; padding: 16px 20px; display: flex; align-items: center; justify-content: space-between; cursor: move; } .gmv-header h3 { margin: 0; font-size: 16px; font-weight: 600; } .gmv-controls { display: flex; gap: 8px; } .gmv-btn { background: rgba(255, 255, 255, 0.2); border: none; border-radius: 8px; color: white; padding: 6px; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; } .gmv-btn:hover { background: rgba(255, 255, 255, 0.3); } .gmv-content { padding: 20px; max-height: calc(90vh - 80px); overflow-y: auto; } .gmv-calculator.minimized .gmv-content, .gmv-calculator.minimized .gmv-header h3, .gmv-calculator.minimized .gmv-controls .gmv-btn:not(.minimize-btn) { display: none; } .angle-selector { margin-bottom: 20px; } .angle-selector label { display: block; margin-bottom: 8px; font-weight: 600; color: #1d1d1f; } .angle-select { width: 100%; padding: 12px 16px; border: 2px solid #e5e5e7; border-radius: 12px; font-size: 14px; background: white; transition: all 0.2s ease; } .angle-select:focus { outline: none; border-color: #007AFF; box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1); } .input-group { margin-bottom: 16px; } .input-group label { display: block; margin-bottom: 6px; font-weight: 500; color: #424245; font-size: 13px; } .input-field { width: 100%; padding: 12px 16px; border: 2px solid #e5e5e7; border-radius: 12px; font-size: 14px; transition: all 0.2s ease; box-sizing: border-box; } .input-field:focus { outline: none; border-color: #007AFF; box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.1); } .calculate-btn { width: 100%; background: linear-gradient(135deg, #007AFF 0%, #5856D6 100%); color: white; border: none; padding: 14px; border-radius: 12px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.2s ease; margin-bottom: 20px; } .calculate-btn:hover { transform: translateY(-1px); box-shadow: 0 10px 20px rgba(0, 122, 255, 0.3); } .result { background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); padding: 16px; border-radius: 12px; margin-bottom: 20px; border-left: 4px solid #007AFF; } .result h4 { margin: 0 0 8px 0; color: #1d1d1f; font-size: 16px; } .result .value { font-size: 24px; font-weight: 700; color: #007AFF; margin: 8px 0; } .result .details { font-size: 12px; color: #86868b; } .history-section { border-top: 1px solid #e5e5e7; padding-top: 20px; margin-top: 20px; } .history-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } .history-header h4 { margin: 0; font-size: 14px; color: #424245; } .clear-history { background: #ff3b30; color: white; border: none; padding: 6px 12px; border-radius: 8px; font-size: 12px; cursor: pointer; } .history-item { background: white; padding: 12px; border-radius: 8px; margin-bottom: 8px; border: 1px solid #e5e5e7; font-size: 12px; } .history-item .time { color: #86868b; margin-bottom: 4px; } .history-item .angle { font-weight: 600; color: #1d1d1f; margin-bottom: 4px; } .history-item .result-value { color: #007AFF; font-weight: 600; } .fade-in { animation: fadeIn 0.3s ease; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } /* 滚动条样式 */ .gmv-content::-webkit-scrollbar { width: 6px; } .gmv-content::-webkit-scrollbar-track { background: transparent; } .gmv-content::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2); border-radius: 3px; } .gmv-content::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.3); } /* GPM 多字段样式 */ .gpm-field-group { background: #f8f9fa; border-radius: 8px; padding: 12px; margin-bottom: 16px; border-left: 3px solid #007AFF; } .gpm-field-group h5 { margin: 0 0 12px 0; font-size: 13px; font-weight: 600; color: #424245; } .gpm-field-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; margin-bottom: 12px; } .gpm-field-row:last-child { margin-bottom: 0; } .gpm-field-item { display: flex; flex-direction: column; } .gpm-field-item label { font-size: 11px; margin-bottom: 4px; color: #86868b; } .gpm-field-item input { padding: 8px 12px; font-size: 12px; border: 1px solid #e5e5e7; border-radius: 6px; } /* 单品多品选择器 */ .product-mode-selector { background: #f0f0f0; border-radius: 8px; padding: 8px; margin-bottom: 16px; display: flex; gap: 4px; } .product-mode-option { flex: 1; padding: 8px 16px; background: transparent; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; color: #424245; transition: all 0.2s ease; } .product-mode-option.active { background: #007AFF; color: white; } .product-mode-option:hover:not(.active) { background: rgba(0, 122, 255, 0.1); } /* 动态商品输入 */ .product-input-section { margin-bottom: 16px; } .product-input-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } .add-product-btn { background: #34c759; color: white; border: none; padding: 6px 12px; border-radius: 6px; font-size: 12px; cursor: pointer; display: flex; align-items: center; gap: 4px; } .add-product-btn:hover { background: #30d158; } .product-item { background: white; border: 1px solid #e5e5e7; border-radius: 8px; padding: 12px; margin-bottom: 8px; position: relative; } .product-item-header { display: flex; justify-content: between; align-items: center; margin-bottom: 8px; } .product-item-title { font-weight: 600; font-size: 13px; color: #424245; } .remove-product-btn { background: #ff3b30; color: white; border: none; padding: 4px 8px; border-radius: 4px; font-size: 11px; cursor: pointer; position: absolute; top: 8px; right: 8px; } .remove-product-btn:hover { background: #ff2d20; } /* 计算结果详情 */ .calculation-details { background: #f8f9fa; border-radius: 8px; padding: 12px; margin-top: 12px; font-size: 12px; } .calculation-step { margin-bottom: 8px; padding: 6px 8px; background: white; border-radius: 4px; border-left: 3px solid #007AFF; } .calculation-step:last-child { margin-bottom: 0; } .calculation-step-title { font-weight: 600; color: #424245; margin-bottom: 2px; } .calculation-step-value { color: #007AFF; font-weight: 600; } `; // 计算角度配置 const calculationAngles = { business: { name: '经营模式角度', formula: 'GMV = 自营GMV + 合作GMV', fields: [ { key: 'self_gmv', label: '自营GMV (元)', type: 'number' }, { key: 'coop_gmv', label: '合作GMV (元)', type: 'number' } ], calculate: (data) => { return { result: (parseFloat(data.self_gmv) || 0) + (parseFloat(data.coop_gmv) || 0), details: `自营GMV: ${formatNumber(data.self_gmv)} + 合作GMV: ${formatNumber(data.coop_gmv)}` }; } }, platform: { name: '三大载体角度', formula: 'GMV = 直播GMV + 短视频GMV + 商品卡GMV', fields: [ { key: 'live_gmv', label: '直播GMV (元)', type: 'number' }, { key: 'video_gmv', label: '短视频GMV (元)', type: 'number' }, { key: 'card_gmv', label: '商品卡GMV (元)', type: 'number' } ], calculate: (data) => { return { result: (parseFloat(data.live_gmv) || 0) + (parseFloat(data.video_gmv) || 0) + (parseFloat(data.card_gmv) || 0), details: `直播: ${formatNumber(data.live_gmv)} + 短视频: ${formatNumber(data.video_gmv)} + 商品卡: ${formatNumber(data.card_gmv)}` }; } }, live_duration: { name: '直播间角度 - 时长维度', formula: 'GMV = 直播时长 × 单小时曝光次数 × 进入率 × 千次观看成交金额 ÷ 1000', fields: [ { key: 'duration', label: '直播时长 (小时)', type: 'number' }, { key: 'hourly_exposure', label: '单小时曝光次数', type: 'number' }, { key: 'entry_rate', label: '进入率 (%)', type: 'number' }, { key: 'cpm_amount', label: '千次观看成交金额 (元)', type: 'number' } ], calculate: (data) => { const result = (parseFloat(data.duration) || 0) * (parseFloat(data.hourly_exposure) || 0) * (parseFloat(data.entry_rate) || 0) / 100 * (parseFloat(data.cpm_amount) || 0) / 1000; return { result: result, details: `${data.duration}小时 × ${formatNumber(data.hourly_exposure)}次曝光 × ${data.entry_rate}%进入率 × ${formatNumber(data.cpm_amount)}元CPM` }; } }, live_exposure: { name: '直播间角度 - 曝光维度', formula: 'GMV = 直播间曝光PV × 千次曝光成交金额 ÷ 1000', fields: [ { key: 'exposure_pv', label: '直播间曝光PV', type: 'number' }, { key: 'cpm_amount', label: '千次曝光成交金额 (元)', type: 'number' } ], calculate: (data) => { const result = (parseFloat(data.exposure_pv) || 0) * (parseFloat(data.cpm_amount) || 0) / 1000; return { result: result, details: `${formatNumber(data.exposure_pv)}PV × ${formatNumber(data.cpm_amount)}元CPM` }; } }, video_traffic: { name: '短视频角度 - 引流', formula: 'GMV = 直播期间曝光次数 × 直播进入率 × 千次观看成交金额 ÷ 1000', fields: [ { key: 'exposure_count', label: '直播期间曝光次数', type: 'number' }, { key: 'live_entry_rate', label: '直播进入率 (%)', type: 'number' }, { key: 'cpm_amount', label: '千次观看成交金额 (元)', type: 'number' } ], calculate: (data) => { const result = (parseFloat(data.exposure_count) || 0) * (parseFloat(data.live_entry_rate) || 0) / 100 * (parseFloat(data.cpm_amount) || 0) / 1000; return { result: result, details: `${formatNumber(data.exposure_count)}次曝光 × ${data.live_entry_rate}%进入率 × ${formatNumber(data.cpm_amount)}元CPM` }; } }, video_cart: { name: '短视频角度 - 挂车', formula: 'GMV = 短视频播放量 × 千次曝光成交金额 ÷ 1000', fields: [ { key: 'play_count', label: '短视频播放量', type: 'number' }, { key: 'cpm_amount', label: '千次曝光成交金额 (元)', type: 'number' } ], calculate: (data) => { const result = (parseFloat(data.play_count) || 0) * (parseFloat(data.cpm_amount) || 0) / 1000; return { result: result, details: `${formatNumber(data.play_count)}播放量 × ${formatNumber(data.cpm_amount)}元CPM` }; } }, product_card: { name: '商品卡角度', formula: 'GMV = 商品卡曝光人数 × 点击率 × 转化率 × 客单价', fields: [ { key: 'exposure_users', label: '商品卡曝光人数', type: 'number' }, { key: 'click_rate', label: '点击率 (%)', type: 'number' }, { key: 'conversion_rate', label: '转化率 (%)', type: 'number' }, { key: 'avg_order_value', label: '客单价 (元)', type: 'number' } ], calculate: (data) => { const result = (parseFloat(data.exposure_users) || 0) * (parseFloat(data.click_rate) || 0) / 100 * (parseFloat(data.conversion_rate) || 0) / 100 * (parseFloat(data.avg_order_value) || 0); return { result: result, details: `${formatNumber(data.exposure_users)}人曝光 × ${data.click_rate}%点击率 × ${data.conversion_rate}%转化率 × ${formatNumber(data.avg_order_value)}元客单价` }; } }, store: { name: '店铺角度', formula: 'GMV = 成交人数 × 人均购买频次 × 客单价 × 单次购买件数', fields: [ { key: 'buyers', label: '成交人数', type: 'number' }, { key: 'purchase_frequency', label: '人均购买频次', type: 'number' }, { key: 'avg_order_value', label: '客单价 (元)', type: 'number' }, { key: 'items_per_order', label: '单次购买件数', type: 'number' } ], calculate: (data) => { const result = (parseFloat(data.buyers) || 0) * (parseFloat(data.purchase_frequency) || 0) * (parseFloat(data.avg_order_value) || 0) * (parseFloat(data.items_per_order) || 0); return { result: result, details: `${formatNumber(data.buyers)}人 × ${data.purchase_frequency}次频次 × ${formatNumber(data.avg_order_value)}元客单价 × ${data.items_per_order}件/次` }; } }, video_gpm: { name: 'GPM计算 - 挂车视频', formula: '总GPM = Σ(各渠道GPM × 流量占比),单渠道GPM = GMV/PV × 1000', fields: [ { key: 'feed_gmv', label: '推荐feed GMV (元)', type: 'number' }, { key: 'feed_pv', label: '推荐feed PV', type: 'number' }, { key: 'feed_ratio', label: '推荐feed流量占比 (%)', type: 'number' }, { key: 'search_gmv', label: '搜索 GMV (元)', type: 'number' }, { key: 'search_pv', label: '搜索 PV', type: 'number' }, { key: 'search_ratio', label: '搜索流量占比 (%)', type: 'number' }, { key: 'other_gmv', label: '其他 GMV (元)', type: 'number' }, { key: 'other_pv', label: '其他 PV', type: 'number' }, { key: 'other_ratio', label: '其他流量占比 (%)', type: 'number' }, { key: 'profile_gmv', label: '个人主页 GMV (元)', type: 'number' }, { key: 'profile_pv', label: '个人主页 PV', type: 'number' }, { key: 'profile_ratio', label: '个人主页流量占比 (%)', type: 'number' }, { key: 'follow_gmv', label: '关注 GMV (元)', type: 'number' }, { key: 'follow_pv', label: '关注 PV', type: 'number' }, { key: 'follow_ratio', label: '关注流量占比 (%)', type: 'number' } ], calculate: (data) => { // 计算各渠道GPM const feedGPM = (parseFloat(data.feed_gmv) || 0) / (parseFloat(data.feed_pv) || 1) * 1000; const searchGPM = (parseFloat(data.search_gmv) || 0) / (parseFloat(data.search_pv) || 1) * 1000; const otherGPM = (parseFloat(data.other_gmv) || 0) / (parseFloat(data.other_pv) || 1) * 1000; const profileGPM = (parseFloat(data.profile_gmv) || 0) / (parseFloat(data.profile_pv) || 1) * 1000; const followGPM = (parseFloat(data.follow_gmv) || 0) / (parseFloat(data.follow_pv) || 1) * 1000; // 计算加权GPM const feedWeightedGPM = feedGPM * (parseFloat(data.feed_ratio) || 0) / 100; const searchWeightedGPM = searchGPM * (parseFloat(data.search_ratio) || 0) / 100; const otherWeightedGPM = otherGPM * (parseFloat(data.other_ratio) || 0) / 100; const profileWeightedGPM = profileGPM * (parseFloat(data.profile_ratio) || 0) / 100; const followWeightedGPM = followGPM * (parseFloat(data.follow_ratio) || 0) / 100; const totalGPM = feedWeightedGPM + searchWeightedGPM + otherWeightedGPM + profileWeightedGPM + followWeightedGPM; return { result: totalGPM, details: `推荐feed: ${feedGPM.toFixed(2)}×${data.feed_ratio}%=${feedWeightedGPM.toFixed(2)} + 搜索: ${searchGPM.toFixed(2)}×${data.search_ratio}%=${searchWeightedGPM.toFixed(2)} + 其他: ${otherGPM.toFixed(2)}×${data.other_ratio}%=${otherWeightedGPM.toFixed(2)} + 主页: ${profileGPM.toFixed(2)}×${data.profile_ratio}%=${profileWeightedGPM.toFixed(2)} + 关注: ${followGPM.toFixed(2)}×${data.follow_ratio}%=${followWeightedGPM.toFixed(2)} = 总GPM: ${totalGPM.toFixed(2)}` }; } }, live_product_gpm: { name: 'GPM计算 - 直播间商品', formula: '商品总GPM = 总GMV/总曝光次数×1000', fields: [ { key: 'live_exposure', label: '直播间曝光次数', type: 'number' }, { key: 'exposure_entry_rate', label: '曝光进入率 (%)', type: 'number' } ], calculate: (data) => { // 动态处理商品数据 const products = []; // 收集所有商品数据 - 遍历所有可能的键 Object.keys(data).forEach(key => { if (key.endsWith('_gmv')) { const index = key.replace('product_', '').replace('_gmv', ''); const gmvKey = `product_${index}_gmv`; const gpmKey = `product_${index}_gpm`; if (data[gmvKey] && data[gpmKey]) { const gmv = parseFloat(data[gmvKey]) || 0; const gpm = parseFloat(data[gpmKey]) || 1; products.push({ gmv, gpm }); } } }); // 如果没有动态商品数据,使用传统的ABCD字段 if (products.length === 0) { ['a', 'b', 'c', 'd'].forEach(letter => { const gmv = parseFloat(data[`product_${letter}_gmv`]) || 0; const gpm = parseFloat(data[`product_${letter}_gpm`]) || 1; if (gmv > 0) { products.push({ gmv, gpm }); } }); } // 1. 计算各商品曝光次数 const productExposures = products.map(product => product.gmv / product.gpm * 1000); // 2. 商品总曝光次数 const totalProductExposure = productExposures.reduce((sum, exposure) => sum + exposure, 0); // 3. 总GMV const totalGMV = products.reduce((sum, product) => sum + product.gmv, 0); // 4. 计算商品总GPM const totalProductGPM = totalProductExposure > 0 ? totalGMV / totalProductExposure * 1000 : 0; // 5. 计算直播间观看次数 const liveViewCount = (parseFloat(data.live_exposure) || 0) * (parseFloat(data.exposure_entry_rate) || 0) / 100; // 6. 计算商品曝光率(次数)= 商品曝光次数/直播间观看次数*100% const productExposureRate = liveViewCount > 0 ? totalProductExposure / liveViewCount * 100 : 0; // 7. 计算千次观看成交金额 = 商品总GPM × 商品曝光率 const cpmAmount = totalProductGPM * (productExposureRate / 100); return { result: totalProductGPM, details: { totalProductExposure: totalProductExposure.toFixed(2), totalProductGPM: totalProductGPM.toFixed(2), liveViewCount: liveViewCount.toFixed(2), productExposureRate: productExposureRate.toFixed(2), cpmAmount: cpmAmount.toFixed(2), totalGMV: totalGMV.toFixed(2), productCount: products.length } }; } } }; // 工具函数 function formatNumber(num) { if (!num) return '0'; return parseFloat(num).toLocaleString('zh-CN'); } function formatCurrency(num) { if (!num) return '¥0.00'; return '¥' + parseFloat(num).toLocaleString('zh-CN', { minimumFractionDigits: 2 }); } function saveCalculation(angle, data, result) { const history = JSON.parse(GM_getValue('gmv_history', '[]')); const calculation = { id: Date.now(), timestamp: new Date().toLocaleString('zh-CN'), angle: calculationAngles[angle].name, data: data, result: result.result, details: result.details }; history.unshift(calculation); // 只保留最近50条记录 if (history.length > 50) { history.splice(50); } GM_setValue('gmv_history', JSON.stringify(history)); } function getHistory() { return JSON.parse(GM_getValue('gmv_history', '[]')); } function clearHistory() { GM_setValue('gmv_history', '[]'); } // 创建UI function createCalculatorUI() { const calculator = document.createElement('div'); calculator.className = 'gmv-calculator'; calculator.innerHTML = ` <div class="gmv-header"> <h3>GMV计算器</h3> <div class="gmv-controls"> <button class="gmv-btn minimize-btn" title="最小化"> <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"> <path d="M2 7h10v1H2V7z"/> </svg> </button> <button class="gmv-btn close-btn" title="关闭"> <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor"> <path d="M7 6.293l4.646-4.647.708.708L7.707 7l4.647 4.646-.708.708L7 7.707 2.354 12.354l-.708-.708L6.293 7 1.646 2.354l.708-.708L7 6.293z"/> </svg> </button> </div> </div> <div class="gmv-content"> <div class="angle-selector"> <label for="angle-select">选择计算角度</label> <select id="angle-select" class="angle-select"> <option value="">请选择计算角度...</option> ${Object.entries(calculationAngles).map(([key, angle]) => `<option value="${key}">${angle.name}</option>` ).join('')} </select> </div> <div id="formula-display" style="display: none; margin-bottom: 20px; padding: 12px; background: #f8f9fa; border-radius: 8px; font-size: 12px; color: #424245;"></div> <div id="input-fields"></div> <button id="calculate-btn" class="calculate-btn" style="display: none;">计算GMV</button> <div id="result-display"></div> <div class="history-section"> <div class="history-header"> <h4>计算历史</h4> <button id="clear-history" class="clear-history">清空</button> </div> <div id="history-list"></div> </div> </div> `; return calculator; } // 初始化 function init() { // 添加样式 const style = document.createElement('style'); style.textContent = styles; document.head.appendChild(style); // 创建计算器 const calculator = createCalculatorUI(); document.body.appendChild(calculator); // 使计算器可拖拽 makeDraggable(calculator); // 绑定事件 bindEvents(calculator); // 加载历史记录 loadHistory(); } // 拖拽功能 function makeDraggable(element) { let isDragging = false; let currentX; let currentY; let initialX; let initialY; let xOffset = 0; let yOffset = 0; const header = element.querySelector('.gmv-header'); header.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', dragEnd); function dragStart(e) { if (e.target.closest('.gmv-controls')) return; initialX = e.clientX - xOffset; initialY = e.clientY - yOffset; if (e.target === header || header.contains(e.target)) { isDragging = true; } } function drag(e) { if (isDragging) { e.preventDefault(); currentX = e.clientX - initialX; currentY = e.clientY - initialY; xOffset = currentX; yOffset = currentY; setTranslate(currentX, currentY, element); } } function dragEnd() { initialX = currentX; initialY = currentY; isDragging = false; } function setTranslate(xPos, yPos, el) { el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`; } } // 绑定事件 function bindEvents(calculator) { const angleSelect = calculator.querySelector('#angle-select'); const calculateBtn = calculator.querySelector('#calculate-btn'); const minimizeBtn = calculator.querySelector('.minimize-btn'); const closeBtn = calculator.querySelector('.close-btn'); const clearHistoryBtn = calculator.querySelector('#clear-history'); // 角度选择 angleSelect.addEventListener('change', function() { const selectedAngle = this.value; if (selectedAngle) { showInputFields(selectedAngle); showFormula(selectedAngle); calculateBtn.style.display = 'block'; } else { hideInputFields(); hideFormula(); calculateBtn.style.display = 'none'; } }); // 计算按钮 calculateBtn.addEventListener('click', function() { const selectedAngle = angleSelect.value; if (selectedAngle) { performCalculation(selectedAngle); } }); // 最小化 minimizeBtn.addEventListener('click', function() { calculator.classList.toggle('minimized'); }); // 关闭 closeBtn.addEventListener('click', function() { calculator.style.display = 'none'; }); // 清空历史 clearHistoryBtn.addEventListener('click', function() { if (confirm('确定要清空所有计算历史吗?')) { clearHistory(); loadHistory(); } }); } // 显示公式 function showFormula(angleKey) { const formulaDisplay = document.querySelector('#formula-display'); const angle = calculationAngles[angleKey]; formulaDisplay.textContent = `计算公式:${angle.formula}`; formulaDisplay.style.display = 'block'; formulaDisplay.classList.add('fade-in'); } // 隐藏公式 function hideFormula() { const formulaDisplay = document.querySelector('#formula-display'); formulaDisplay.style.display = 'none'; } // 显示输入字段 function showInputFields(angleKey) { const inputFields = document.querySelector('#input-fields'); const angle = calculationAngles[angleKey]; // GPM计算特殊布局 if (angleKey === 'video_gpm') { inputFields.innerHTML = ` <div class="gpm-field-group"> <h5>推荐Feed流量</h5> <div class="gpm-field-row"> <div class="gpm-field-item"> <label for="feed_gmv">GMV (元)</label> <input type="number" id="feed_gmv" class="input-field" placeholder="133905.98"> </div> <div class="gpm-field-item"> <label for="feed_pv">PV</label> <input type="number" id="feed_pv" class="input-field" placeholder="1485690"> </div> <div class="gpm-field-item"> <label for="feed_ratio">流量占比 (%)</label> <input type="number" id="feed_ratio" class="input-field" placeholder="82"> </div> </div> </div> <div class="gpm-field-group"> <h5>搜索流量</h5> <div class="gpm-field-row"> <div class="gpm-field-item"> <label for="search_gmv">GMV (元)</label> <input type="number" id="search_gmv" class="input-field" placeholder="23799.57"> </div> <div class="gpm-field-item"> <label for="search_pv">PV</label> <input type="number" id="search_pv" class="input-field" placeholder="146361"> </div> <div class="gpm-field-item"> <label for="search_ratio">流量占比 (%)</label> <input type="number" id="search_ratio" class="input-field" placeholder="8"> </div> </div> </div> <div class="gpm-field-group"> <h5>其他流量</h5> <div class="gpm-field-row"> <div class="gpm-field-item"> <label for="other_gmv">GMV (元)</label> <input type="number" id="other_gmv" class="input-field" placeholder="15529.2"> </div> <div class="gpm-field-item"> <label for="other_pv">PV</label> <input type="number" id="other_pv" class="input-field" placeholder="108985"> </div> <div class="gpm-field-item"> <label for="other_ratio">流量占比 (%)</label> <input type="number" id="other_ratio" class="input-field" placeholder="6"> </div> </div> </div> <div class="gpm-field-group"> <h5>个人主页流量</h5> <div class="gpm-field-row"> <div class="gpm-field-item"> <label for="profile_gmv">GMV (元)</label> <input type="number" id="profile_gmv" class="input-field" placeholder="8416.7"> </div> <div class="gpm-field-item"> <label for="profile_pv">PV</label> <input type="number" id="profile_pv" class="input-field" placeholder="44707"> </div> <div class="gpm-field-item"> <label for="profile_ratio">流量占比 (%)</label> <input type="number" id="profile_ratio" class="input-field" placeholder="2"> </div> </div> </div> <div class="gpm-field-group"> <h5>关注流量</h5> <div class="gpm-field-row"> <div class="gpm-field-item"> <label for="follow_gmv">GMV (元)</label> <input type="number" id="follow_gmv" class="input-field" placeholder="3186.3"> </div> <div class="gpm-field-item"> <label for="follow_pv">PV</label> <input type="number" id="follow_pv" class="input-field" placeholder="20428"> </div> <div class="gpm-field-item"> <label for="follow_ratio">流量占比 (%)</label> <input type="number" id="follow_ratio" class="input-field" placeholder="1"> </div> </div> </div> `; } else if (angleKey === 'live_product_gpm') { inputFields.innerHTML = ` <div class="product-mode-selector"> <button type="button" class="product-mode-option active" data-mode="single">单品计算</button> <button type="button" class="product-mode-option" data-mode="multiple">多品计算</button> </div> <div id="single-product-section" class="product-input-section"> <div class="gpm-field-group"> <h5>单品数据</h5> <div class="gpm-field-row"> <div class="gpm-field-item"> <label for="single_product_gmv">商品GMV (元)</label> <input type="number" id="single_product_gmv" class="input-field" placeholder="4453.6"> </div> <div class="gpm-field-item"> <label for="single_product_gpm">商品GPM</label> <input type="number" id="single_product_gpm" class="input-field" placeholder="574.51"> </div> <div class="gpm-field-item"></div> </div> </div> </div> <div id="multiple-product-section" class="product-input-section" style="display: none;"> <div class="product-input-header"> <h5>多商品数据</h5> <button type="button" class="add-product-btn" id="add-product-btn"> <span>+</span> 添加商品 </button> </div> <div id="products-container"> <div class="product-item" data-index="0"> <button type="button" class="remove-product-btn" data-index="0" style="display: none;">删除</button> <div class="product-item-header"> <span class="product-item-title">商品 1</span> </div> <div class="gpm-field-row"> <div class="gpm-field-item"> <label for="product_0_gmv">GMV (元)</label> <input type="number" id="product_0_gmv" class="input-field" placeholder="4453.6"> </div> <div class="gpm-field-item"> <label for="product_0_gpm">GPM</label> <input type="number" id="product_0_gpm" class="input-field" placeholder="574.51"> </div> <div class="gpm-field-item"></div> </div> </div> </div> </div> <div class="gpm-field-group"> <h5>直播间数据</h5> <div class="gpm-field-row"> <div class="gpm-field-item"> <label for="live_exposure">直播间曝光次数</label> <input type="number" id="live_exposure" class="input-field" placeholder="请输入直播间曝光次数"> </div> <div class="gpm-field-item"> <label for="exposure_entry_rate">曝光进入率 (%)</label> <input type="number" id="exposure_entry_rate" class="input-field" placeholder="请输入曝光进入率"> </div> <div class="gpm-field-item"></div> </div> </div> `; // 重置商品计数器并绑定模式切换事件 setTimeout(() => { resetProductCount(); bindProductModeEvents(); }, 100); } else { // 默认布局 inputFields.innerHTML = angle.fields.map(field => ` <div class="input-group"> <label for="${field.key}">${field.label}</label> <input type="${field.type}" id="${field.key}" class="input-field" placeholder="请输入${field.label}"> </div> `).join(''); } inputFields.classList.add('fade-in'); } // 隐藏输入字段 function hideInputFields() { const inputFields = document.querySelector('#input-fields'); inputFields.innerHTML = ''; } // 执行计算 function performCalculation(angleKey) { const angle = calculationAngles[angleKey]; const data = {}; let isValid = true; // 特殊处理直播间商品GPM计算 if (angleKey === 'live_product_gpm') { // 收集直播间数据 const liveExposure = document.querySelector('#live_exposure'); const entryRate = document.querySelector('#exposure_entry_rate'); if (!liveExposure || !entryRate || !liveExposure.value.trim() || !entryRate.value.trim()) { alert('请填写直播间数据'); return; } data.live_exposure = liveExposure.value; data.exposure_entry_rate = entryRate.value; // 直接尝试收集所有可能的商品数据 let hasProductData = false; // 尝试单品模式数据 const singleGmv = document.querySelector('#single_product_gmv'); const singleGpm = document.querySelector('#single_product_gpm'); if (singleGmv && singleGpm && singleGmv.value.trim() && singleGpm.value.trim()) { data.product_0_gmv = singleGmv.value; data.product_0_gpm = singleGpm.value; hasProductData = true; } // 尝试收集多品模式数据(product_0, product_1, etc.) for (let i = 0; i < 20; i++) { // 最多检查20个商品 const gmvInput = document.querySelector(`#product_${i}_gmv`); const gpmInput = document.querySelector(`#product_${i}_gpm`); if (gmvInput && gpmInput && gmvInput.value.trim() && gpmInput.value.trim()) { data[`product_${i}_gmv`] = gmvInput.value; data[`product_${i}_gpm`] = gpmInput.value; hasProductData = true; } } if (!hasProductData) { alert('请填写商品数据(GMV和GPM都必须填写)'); return; } } else { // 标准字段收集 angle.fields.forEach(field => { const input = document.querySelector(`#${field.key}`); const value = input.value.trim(); if (!value) { isValid = false; input.style.borderColor = '#ff3b30'; setTimeout(() => { input.style.borderColor = '#e5e5e7'; }, 2000); } else { data[field.key] = value; } }); if (!isValid) { alert('请填写所有必需字段'); return; } } // 执行计算 const result = angle.calculate(data); // 保存计算记录 saveCalculation(angleKey, data, result); // 显示结果 showResult(result, angle.name); // 刷新历史记录 loadHistory(); } // 显示结果 function showResult(result, angleName) { const resultDisplay = document.querySelector('#result-display'); const isGPM = angleName.includes('GPM'); const resultValue = isGPM ? `${result.result.toFixed(2)} GPM` : formatCurrency(result.result); let detailsHtml = ''; // 特殊处理直播间商品GPM的详细计算步骤 if (angleName.includes('直播间商品') && typeof result.details === 'object') { detailsHtml = ` <div class="calculation-details"> <div class="calculation-step"> <div class="calculation-step-title">1. 商品总曝光次数</div> <div class="calculation-step-value">${result.details.totalProductExposure} 次</div> </div> <div class="calculation-step"> <div class="calculation-step-title">2. 商品总GMV</div> <div class="calculation-step-value">¥${result.details.totalGMV}</div> </div> <div class="calculation-step"> <div class="calculation-step-title">3. 计算商品总GPM</div> <div class="calculation-step-value">${result.details.totalProductGPM} GPM</div> </div> <div class="calculation-step"> <div class="calculation-step-title">4. 直播间观看次数</div> <div class="calculation-step-value">${result.details.liveViewCount} 次</div> </div> <div class="calculation-step"> <div class="calculation-step-title">5. 商品曝光率</div> <div class="calculation-step-value">${result.details.productExposureRate}%</div> </div> <div class="calculation-step"> <div class="calculation-step-title">6. 千次观看成交金额</div> <div class="calculation-step-value">¥${result.details.cpmAmount}</div> <div style="font-size: 10px; color: #86868b; margin-top: 2px;"> ${result.details.totalProductGPM} × ${result.details.productExposureRate}% = ${result.details.cpmAmount} </div> </div> <div style="text-align: center; margin-top: 8px; font-size: 11px; color: #86868b;"> 基于 ${result.details.productCount} 个商品的数据计算 </div> </div> `; } else { detailsHtml = `<div class="details">${result.details}</div>`; } resultDisplay.innerHTML = ` <div class="result fade-in"> <h4>${angleName} - 计算结果</h4> <div class="value">${resultValue}</div> ${detailsHtml} </div> `; } // 加载历史记录 function loadHistory() { const historyList = document.querySelector('#history-list'); const history = getHistory(); if (history.length === 0) { historyList.innerHTML = '<div style="text-align: center; color: #86868b; padding: 20px; font-size: 12px;">暂无计算历史</div>'; return; } historyList.innerHTML = history.slice(0, 10).map(item => { const isGPM = item.angle.includes('GPM'); const resultValue = isGPM ? `${item.result.toFixed(2)} GPM` : formatCurrency(item.result); return ` <div class="history-item"> <div class="time">${item.timestamp}</div> <div class="angle">${item.angle}</div> <div class="result-value">${resultValue}</div> </div> `; }).join(''); } // 启动脚本 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // 添加快捷键支持 document.addEventListener('keydown', function(e) { if (e.altKey && e.key === 'g') { const calculator = document.querySelector('.gmv-calculator'); if (calculator) { calculator.style.display = calculator.style.display === 'none' ? 'block' : 'none'; } } }); // 商品管理函数 let productCount = 1; // 重置商品计数器 function resetProductCount() { productCount = 1; } window.bindProductModeEvents = function() { const modeButtons = document.querySelectorAll('.product-mode-option'); const singleSection = document.getElementById('single-product-section'); const multipleSection = document.getElementById('multiple-product-section'); const addProductBtn = document.getElementById('add-product-btn'); // 模式切换事件 modeButtons.forEach(btn => { btn.addEventListener('click', function() { modeButtons.forEach(b => b.classList.remove('active')); this.classList.add('active'); const mode = this.dataset.mode; if (mode === 'single') { singleSection.style.display = 'block'; multipleSection.style.display = 'none'; } else { singleSection.style.display = 'none'; multipleSection.style.display = 'block'; } }); }); // 添加商品按钮事件 if (addProductBtn) { addProductBtn.addEventListener('click', addProductField); } // 删除商品按钮事件(事件委托) const productsContainer = document.getElementById('products-container'); if (productsContainer) { productsContainer.addEventListener('click', function(e) { if (e.target.classList.contains('remove-product-btn')) { const index = e.target.dataset.index; removeProductField(index); } }); } }; window.addProductField = function() { const container = document.getElementById('products-container'); const productItem = document.createElement('div'); productItem.className = 'product-item'; productItem.dataset.index = productCount; productItem.innerHTML = ` <button type="button" class="remove-product-btn" data-index="${productCount}">删除</button> <div class="product-item-header"> <span class="product-item-title">商品 ${productCount + 1}</span> </div> <div class="gpm-field-row"> <div class="gpm-field-item"> <label for="product_${productCount}_gmv">GMV (元)</label> <input type="number" id="product_${productCount}_gmv" class="input-field" placeholder="请输入GMV"> </div> <div class="gpm-field-item"> <label for="product_${productCount}_gpm">GPM</label> <input type="number" id="product_${productCount}_gpm" class="input-field" placeholder="请输入GPM"> </div> <div class="gpm-field-item"></div> </div> `; container.appendChild(productItem); productCount++; // 显示第一个商品的删除按钮 const firstProduct = document.querySelector('.product-item[data-index="0"] .remove-product-btn'); if (firstProduct && productCount > 1) { firstProduct.style.display = 'block'; } }; window.removeProductField = function(index) { const productItem = document.querySelector(`.product-item[data-index="${index}"]`); if (productItem) { productItem.remove(); // 重新编号剩余商品 const remainingProducts = document.querySelectorAll('.product-item'); remainingProducts.forEach((product, idx) => { const title = product.querySelector('.product-item-title'); if (title) { title.textContent = `商品 ${idx + 1}`; } }); // 如果只剩一个商品,隐藏删除按钮 if (remainingProducts.length <= 1) { const firstDeleteBtn = document.querySelector('.product-item .remove-product-btn'); if (firstDeleteBtn) { firstDeleteBtn.style.display = 'none'; } } } }; })();