Spaces:
Sleeping
Sleeping
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>增长飞轮模拟器 (Growth Loop Simulator)</title> | |
| <script src="{{ url_for('static', filename='js/vue.global.js') }}"></script> | |
| <script src="{{ url_for('static', filename='js/tailwindcss.js') }}"></script> | |
| <script src="{{ url_for('static', filename='js/chart.js') }}"></script> | |
| <script src="{{ url_for('static', filename='js/html2canvas.min.js') }}"></script> | |
| <link href="{{ url_for('static', filename='css/all.min.css') }}" rel="stylesheet"> | |
| <script> | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| colors: { | |
| primary: '#6366f1', | |
| secondary: '#ec4899', | |
| dark: '#0f172a', | |
| darker: '#020617', | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); | |
| body { font-family: 'Inter', sans-serif; } | |
| .slider-thumb::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 16px; | |
| height: 16px; | |
| background: #6366f1; | |
| cursor: pointer; | |
| border-radius: 50%; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 text-gray-900 dark:bg-darker dark:text-gray-100 transition-colors duration-300"> | |
| {% raw %} | |
| <div id="app" class="min-h-screen flex flex-col"> | |
| <!-- Header --> | |
| <header class="bg-white dark:bg-dark border-b border-gray-200 dark:border-gray-800 sticky top-0 z-50"> | |
| <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between"> | |
| <div class="flex items-center space-x-3"> | |
| <div class="w-10 h-10 bg-gradient-to-br from-primary to-secondary rounded-lg flex items-center justify-center text-white font-bold text-xl shadow-lg"> | |
| <i class="fa-solid fa-rocket"></i> | |
| </div> | |
| <h1 class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-secondary"> | |
| 增长飞轮模拟器 | |
| </h1> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <button @click="toggleTheme" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"> | |
| <i :class="isDark ? 'fa-solid fa-sun text-yellow-400' : 'fa-solid fa-moon text-gray-600'"></i> | |
| </button> | |
| <button @click="exportReport" class="px-4 py-2 bg-primary hover:bg-indigo-600 text-white rounded-lg text-sm font-medium transition-colors shadow-md flex items-center gap-2"> | |
| <i class="fa-solid fa-download"></i> 导出报告 | |
| </button> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="flex-1 max-w-7xl mx-auto w-full p-4 sm:p-6 lg:p-8 flex flex-col lg:flex-row gap-6"> | |
| <!-- Left Sidebar: Controls --> | |
| <div class="w-full lg:w-1/3 space-y-6 overflow-y-auto max-h-[calc(100vh-8rem)] pr-2 custom-scrollbar"> | |
| <!-- Presets --> | |
| <div class="bg-white dark:bg-dark p-5 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800"> | |
| <h2 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">商业模式预设</h2> | |
| <div class="grid grid-cols-3 gap-2"> | |
| <button @click="applyPreset('plg')" :class="preset === 'plg' ? 'bg-primary text-white border-primary' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-transparent hover:border-primary'" class="p-2 rounded-lg text-xs font-medium border transition-all"> | |
| PLG (产品驱动) | |
| </button> | |
| <button @click="applyPreset('slg')" :class="preset === 'slg' ? 'bg-secondary text-white border-secondary' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-transparent hover:border-secondary'" class="p-2 rounded-lg text-xs font-medium border transition-all"> | |
| SLG (销售驱动) | |
| </button> | |
| <button @click="applyPreset('viral')" :class="preset === 'viral' ? 'bg-green-500 text-white border-green-500' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-transparent hover:border-green-500'" class="p-2 rounded-lg text-xs font-medium border transition-all"> | |
| 病毒式传播 | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Input Groups --> | |
| <div class="space-y-4"> | |
| <!-- Acquisition --> | |
| <div class="bg-white dark:bg-dark p-5 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800 transition-all hover:shadow-md"> | |
| <div class="flex items-center gap-2 mb-4 text-primary"> | |
| <i class="fa-solid fa-magnet"></i> | |
| <h3 class="font-bold">获客 (Acquisition)</h3> | |
| </div> | |
| <control-slider label="月度广告预算 ($)" v-model="params.adBudget" :min="0" :max="50000" :step="500"></control-slider> | |
| <control-slider label="CPC (单次点击成本 $)" v-model="params.cpc" :min="0.1" :max="50" :step="0.1"></control-slider> | |
| <control-slider label="自然流量 (月访客)" v-model="params.organicTraffic" :min="0" :max="10000" :step="100"></control-slider> | |
| </div> | |
| <!-- Activation --> | |
| <div class="bg-white dark:bg-dark p-5 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800 transition-all hover:shadow-md"> | |
| <div class="flex items-center gap-2 mb-4 text-blue-500"> | |
| <i class="fa-solid fa-bolt"></i> | |
| <h3 class="font-bold">激活 (Activation)</h3> | |
| </div> | |
| <control-slider label="访客注册率 (%)" v-model="params.visitorToSignup" :min="0.1" :max="50" :step="0.1"></control-slider> | |
| <control-slider label="注册激活率 (%)" v-model="params.signupToActive" :min="1" :max="100" :step="1"></control-slider> | |
| </div> | |
| <!-- Retention --> | |
| <div class="bg-white dark:bg-dark p-5 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800 transition-all hover:shadow-md"> | |
| <div class="flex items-center gap-2 mb-4 text-red-500"> | |
| <i class="fa-solid fa-heart-crack"></i> | |
| <h3 class="font-bold">留存 (Retention)</h3> | |
| </div> | |
| <control-slider label="月流失率 (%)" v-model="params.churnRate" :min="0.1" :max="30" :step="0.1" tooltip="越低越好"></control-slider> | |
| </div> | |
| <!-- Referral --> | |
| <div class="bg-white dark:bg-dark p-5 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800 transition-all hover:shadow-md"> | |
| <div class="flex items-center gap-2 mb-4 text-green-500"> | |
| <i class="fa-solid fa-users-viewfinder"></i> | |
| <h3 class="font-bold">推荐 (Referral)</h3> | |
| </div> | |
| <control-slider label="病毒系数 (K-Factor)" v-model="params.viralCoefficient" :min="0" :max="2" :step="0.05" tooltip=">1 为自增长"></control-slider> | |
| </div> | |
| <!-- Revenue --> | |
| <div class="bg-white dark:bg-dark p-5 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800 transition-all hover:shadow-md"> | |
| <div class="flex items-center gap-2 mb-4 text-yellow-500"> | |
| <i class="fa-solid fa-coins"></i> | |
| <h3 class="font-bold">营收 (Revenue)</h3> | |
| </div> | |
| <control-slider label="ARPU (平均用户月收入 $)" v-model="params.arpu" :min="0" :max="500" :step="1"></control-slider> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right Content: Charts & Metrics --> | |
| <div class="w-full lg:w-2/3 flex flex-col gap-6" id="report-area"> | |
| <!-- Key Metrics Cards --> | |
| <div class="grid grid-cols-2 md:grid-cols-4 gap-4"> | |
| <metric-card title="第12月 MRR" :value="formatCurrency(month12Data.mrr)" icon="fa-chart-line" color="text-primary"></metric-card> | |
| <metric-card title="第12月 活跃用户" :value="formatNumber(month12Data.activeUsers)" icon="fa-users" color="text-blue-500"></metric-card> | |
| <metric-card title="LTV (生命周期价值)" :value="formatCurrency(ltv)" icon="fa-gem" color="text-purple-500"></metric-card> | |
| <metric-card title="CAC (获客成本)" :value="formatCurrency(cac)" icon="fa-hand-holding-dollar" :color="ltvToCac > 3 ? 'text-green-500' : 'text-red-500'"></metric-card> | |
| </div> | |
| <!-- North Star Metric --> | |
| <div class="bg-gradient-to-r from-indigo-500 to-purple-600 rounded-xl p-6 text-white shadow-lg flex justify-between items-center"> | |
| <div> | |
| <div class="text-indigo-100 text-sm font-medium mb-1">健康度指标 (LTV / CAC)</div> | |
| <div class="text-3xl font-bold flex items-baseline gap-2"> | |
| {{ ltvToCac.toFixed(2) }}x | |
| <span class="text-sm font-normal bg-white/20 px-2 py-0.5 rounded" v-if="ltvToCac > 3">健康 🚀</span> | |
| <span class="text-sm font-normal bg-red-500/50 px-2 py-0.5 rounded" v-else>危险 ⚠️</span> | |
| </div> | |
| </div> | |
| <div class="text-right"> | |
| <div class="text-indigo-100 text-sm font-medium mb-1">达到 $10k MRR</div> | |
| <div class="text-2xl font-bold">{{ monthTo10k > 0 ? `第 ${monthTo10k} 个月` : '未达成' }}</div> | |
| </div> | |
| </div> | |
| <!-- Main Chart --> | |
| <div class="bg-white dark:bg-dark p-6 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800 flex-1 min-h-[400px]"> | |
| <h3 class="text-lg font-bold mb-4">24个月增长预测</h3> | |
| <div class="relative h-[350px] w-full"> | |
| <canvas id="growthChart"></canvas> | |
| </div> | |
| </div> | |
| <!-- Breakdown Chart --> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div class="bg-white dark:bg-dark p-6 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800"> | |
| <h3 class="text-lg font-bold mb-4">用户来源构成 (第12月)</h3> | |
| <div class="relative h-[250px] w-full flex justify-center"> | |
| <canvas id="sourceChart"></canvas> | |
| </div> | |
| </div> | |
| <div class="bg-white dark:bg-dark p-6 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800"> | |
| <h3 class="text-lg font-bold mb-4">关键洞察</h3> | |
| <ul class="space-y-3 text-sm"> | |
| <li class="flex items-start gap-2"> | |
| <i class="fa-solid fa-check-circle text-green-500 mt-1"></i> | |
| <span>如果流失率降低 1%,第24个月收入将增加 <span class="font-bold text-green-600">{{ formatCurrency(churnImpact) }}</span></span> | |
| </li> | |
| <li class="flex items-start gap-2"> | |
| <i class="fa-solid fa-check-circle text-blue-500 mt-1"></i> | |
| <span>当前付费回本周期 (Payback Period): <span class="font-bold">{{ paybackPeriod.toFixed(1) }} 个月</span></span> | |
| </li> | |
| <li class="flex items-start gap-2" v-if="params.viralCoefficient > 0.5"> | |
| <i class="fa-solid fa-fire text-orange-500 mt-1"></i> | |
| <span>病毒系数较高,建议加大顶部流量漏斗。</span> | |
| </li> | |
| <li class="flex items-start gap-2" v-if="params.churnRate > 5"> | |
| <i class="fa-solid fa-triangle-exclamation text-red-500 mt-1"></i> | |
| <span>流失率过高 (>5%),这应该是首要解决的问题。</span> | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- Components --> | |
| <script type="text/x-template" id="control-slider-template"> | |
| </script> | |
| <script type="text/x-template" id="metric-card-template"> | |
| </script> | |
| <script> | |
| const { createApp, ref, reactive, computed, watch, onMounted, nextTick } = Vue; | |
| const ControlSlider = { | |
| props: ['label', 'modelValue', 'min', 'max', 'step', 'tooltip'], | |
| emits: ['update:modelValue'], | |
| template: '#control-slider-template' | |
| }; | |
| const MetricCard = { | |
| props: ['title', 'value', 'icon', 'color'], | |
| template: '#metric-card-template' | |
| }; | |
| createApp({ | |
| components: { | |
| 'control-slider': ControlSlider, | |
| 'metric-card': MetricCard | |
| }, | |
| setup() { | |
| const isDark = ref(false); | |
| const preset = ref('plg'); | |
| // Initial Params | |
| const params = reactive({ | |
| adBudget: 2000, | |
| cpc: 2.5, | |
| organicTraffic: 500, | |
| visitorToSignup: 5.0, | |
| signupToActive: 40, | |
| churnRate: 3.5, | |
| viralCoefficient: 0.1, | |
| arpu: 29 | |
| }); | |
| // Simulation Logic | |
| const simulation = computed(() => { | |
| let months = []; | |
| let activeUsers = 0; | |
| // Monthly data | |
| for (let i = 0; i < 24; i++) { | |
| // 1. New Users Calculation | |
| const paidTraffic = params.adBudget / Math.max(0.01, params.cpc); | |
| const totalTraffic = paidTraffic + params.organicTraffic; | |
| const signups = totalTraffic * (params.visitorToSignup / 100); | |
| const newActiveFromFunnel = signups * (params.signupToActive / 100); | |
| // 2. Viral Growth | |
| const viralNewUsers = activeUsers * params.viralCoefficient; | |
| const totalNewUsers = newActiveFromFunnel + viralNewUsers; | |
| // 3. Churn | |
| const churnedUsers = activeUsers * (params.churnRate / 100); | |
| // 4. Update Active Users | |
| activeUsers = Math.max(0, activeUsers + totalNewUsers - churnedUsers); | |
| // 5. Revenue | |
| const mrr = activeUsers * params.arpu; | |
| months.push({ | |
| month: i + 1, | |
| activeUsers: Math.round(activeUsers), | |
| mrr: Math.round(mrr), | |
| newUsers: Math.round(totalNewUsers), | |
| paidUsers: Math.round(newActiveFromFunnel), // Approximation for chart | |
| viralUsers: Math.round(viralNewUsers) | |
| }); | |
| } | |
| return months; | |
| }); | |
| // Computed Metrics | |
| const month12Data = computed(() => simulation.value[11] || { mrr: 0, activeUsers: 0 }); | |
| const month24Data = computed(() => simulation.value[23] || { mrr: 0, activeUsers: 0 }); | |
| const cac = computed(() => { | |
| const paidTraffic = params.adBudget / Math.max(0.01, params.cpc); | |
| const signups = paidTraffic * (params.visitorToSignup / 100); | |
| const paidCustomers = signups * (params.signupToActive / 100); | |
| if (paidCustomers <= 0) return 0; | |
| return params.adBudget / paidCustomers; | |
| }); | |
| const ltv = computed(() => { | |
| if (params.churnRate <= 0) return 0; | |
| return params.arpu / (params.churnRate / 100); | |
| }); | |
| const ltvToCac = computed(() => { | |
| if (cac.value <= 0) return 0; | |
| return ltv.value / cac.value; | |
| }); | |
| const monthTo10k = computed(() => { | |
| const found = simulation.value.find(m => m.mrr >= 10000); | |
| return found ? found.month : 0; | |
| }); | |
| const paybackPeriod = computed(() => { | |
| if (params.arpu <= 0) return 0; | |
| return cac.value / params.arpu; | |
| }); | |
| const churnImpact = computed(() => { | |
| // Simulate with 1% less churn | |
| let active = 0; | |
| for (let i = 0; i < 24; i++) { | |
| const paidTraffic = params.adBudget / Math.max(0.01, params.cpc); | |
| const totalTraffic = paidTraffic + params.organicTraffic; | |
| const signups = totalTraffic * (params.visitorToSignup / 100); | |
| const newActive = signups * (params.signupToActive / 100); | |
| const viral = active * params.viralCoefficient; | |
| const totalNew = newActive + viral; | |
| const churn = active * (Math.max(0, params.churnRate - 1) / 100); | |
| active = active + totalNew - churn; | |
| } | |
| const newMRR = active * params.arpu; | |
| return newMRR - month24Data.value.mrr; | |
| }); | |
| // Chart Instances | |
| let growthChartInstance = null; | |
| let sourceChartInstance = null; | |
| const updateCharts = () => { | |
| if (!growthChartInstance || !sourceChartInstance) return; | |
| const labels = simulation.value.map(d => `M${d.month}`); | |
| const mrrData = simulation.value.map(d => d.mrr); | |
| const userData = simulation.value.map(d => d.activeUsers); | |
| // Update Growth Chart | |
| growthChartInstance.data.labels = labels; | |
| growthChartInstance.data.datasets[0].data = mrrData; | |
| growthChartInstance.data.datasets[1].data = userData; | |
| growthChartInstance.update(); | |
| // Update Source Chart (Month 12 snapshot) | |
| const m12 = simulation.value[11]; | |
| if (m12) { | |
| sourceChartInstance.data.datasets[0].data = [m12.paidUsers, m12.viralUsers]; | |
| sourceChartInstance.update(); | |
| } | |
| }; | |
| const initCharts = () => { | |
| const ctxGrowth = document.getElementById('growthChart').getContext('2d'); | |
| const ctxSource = document.getElementById('sourceChart').getContext('2d'); | |
| Chart.defaults.color = isDark.value ? '#94a3b8' : '#64748b'; | |
| Chart.defaults.borderColor = isDark.value ? '#334155' : '#e2e8f0'; | |
| growthChartInstance = new Chart(ctxGrowth, { | |
| type: 'line', | |
| data: { | |
| labels: [], | |
| datasets: [ | |
| { | |
| label: 'MRR ($)', | |
| data: [], | |
| borderColor: '#6366f1', | |
| backgroundColor: 'rgba(99, 102, 241, 0.1)', | |
| yAxisID: 'y', | |
| fill: true, | |
| tension: 0.4 | |
| }, | |
| { | |
| label: '活跃用户', | |
| data: [], | |
| borderColor: '#ec4899', | |
| backgroundColor: 'rgba(236, 72, 153, 0.1)', | |
| yAxisID: 'y1', | |
| fill: true, | |
| tension: 0.4 | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| interaction: { | |
| mode: 'index', | |
| intersect: false, | |
| }, | |
| scales: { | |
| y: { | |
| type: 'linear', | |
| display: true, | |
| position: 'left', | |
| title: { display: true, text: 'MRR ($)' } | |
| }, | |
| y1: { | |
| type: 'linear', | |
| display: true, | |
| position: 'right', | |
| grid: { drawOnChartArea: false }, | |
| title: { display: true, text: 'Users' } | |
| } | |
| } | |
| } | |
| }); | |
| sourceChartInstance = new Chart(ctxSource, { | |
| type: 'doughnut', | |
| data: { | |
| labels: ['渠道/付费', '病毒/推荐'], | |
| datasets: [{ | |
| data: [50, 50], | |
| backgroundColor: ['#6366f1', '#10b981'], | |
| borderWidth: 0 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { position: 'bottom' } | |
| } | |
| } | |
| }); | |
| }; | |
| // Presets Logic | |
| const presets = { | |
| plg: { adBudget: 1000, cpc: 2, organicTraffic: 2000, visitorToSignup: 8, signupToActive: 60, churnRate: 4, viralCoefficient: 0.15, arpu: 15 }, | |
| slg: { adBudget: 5000, cpc: 10, organicTraffic: 200, visitorToSignup: 2, signupToActive: 20, churnRate: 1.5, viralCoefficient: 0.05, arpu: 200 }, | |
| viral: { adBudget: 500, cpc: 0.5, organicTraffic: 5000, visitorToSignup: 15, signupToActive: 50, churnRate: 8, viralCoefficient: 0.8, arpu: 5 } | |
| }; | |
| const applyPreset = (key) => { | |
| preset.value = key; | |
| Object.assign(params, presets[key]); | |
| }; | |
| // Watchers & Lifecycle | |
| watch(params, () => updateCharts(), { deep: true }); | |
| watch(isDark, () => { | |
| if(growthChartInstance) { | |
| Chart.defaults.color = isDark.value ? '#94a3b8' : '#64748b'; | |
| Chart.defaults.borderColor = isDark.value ? '#334155' : '#e2e8f0'; | |
| growthChartInstance.update(); | |
| sourceChartInstance.update(); | |
| } | |
| if (isDark.value) { | |
| document.documentElement.classList.add('dark'); | |
| } else { | |
| document.documentElement.classList.remove('dark'); | |
| } | |
| }); | |
| onMounted(() => { | |
| // Check system preference | |
| if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { | |
| isDark.value = true; | |
| } | |
| nextTick(() => { | |
| initCharts(); | |
| updateCharts(); | |
| }); | |
| }); | |
| const toggleTheme = () => isDark.value = !isDark.value; | |
| const formatCurrency = (val) => { | |
| return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(val); | |
| }; | |
| const formatNumber = (val) => { | |
| return new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(val); | |
| }; | |
| const exportReport = async () => { | |
| const element = document.getElementById('report-area'); | |
| try { | |
| const canvas = await html2canvas(element, { | |
| backgroundColor: isDark.value ? '#0f172a' : '#ffffff', | |
| scale: 2 | |
| }); | |
| const link = document.createElement('a'); | |
| link.download = `growth-simulation-${new Date().toISOString().slice(0,10)}.png`; | |
| link.href = canvas.toDataURL(); | |
| link.click(); | |
| } catch (err) { | |
| console.error("Export failed", err); | |
| alert("导出失败,请重试"); | |
| } | |
| }; | |
| return { | |
| isDark, toggleTheme, params, preset, applyPreset, | |
| simulation, month12Data, month24Data, | |
| cac, ltv, ltvToCac, monthTo10k, paybackPeriod, churnImpact, | |
| formatCurrency, formatNumber, exportReport | |
| }; | |
| } | |
| }).mount('#app'); | |
| </script> | |
| {% endraw %} | |
| </body> | |
| </html> | |