Trae Assistant
feat: enhance robustness, add import functionality, and prepare for HF deployment
727abcc | <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>转化漏斗架构师 (Conversion Funnel Architect)</title> | |
| <!-- Vue 3 --> | |
| <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- ECharts --> | |
| <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> | |
| <!-- Font Awesome --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| body { font-family: 'Inter', sans-serif; background-color: #f3f4f6; } | |
| .chart-container { height: 400px; width: 100%; } | |
| /* Custom scrollbar for cleaner look */ | |
| ::-webkit-scrollbar { width: 8px; } | |
| ::-webkit-scrollbar-track { background: #f1f1f1; } | |
| ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; } | |
| ::-webkit-scrollbar-thumb:hover { background: #94a3b8; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="app" class="flex h-screen overflow-hidden"> | |
| <!-- Sidebar: Configuration --> | |
| <div class="w-80 bg-white border-r border-gray-200 flex flex-col h-full shadow-lg z-10"> | |
| <div class="p-6 border-b border-gray-100"> | |
| <h1 class="text-xl font-bold text-gray-800 flex items-center gap-2"> | |
| <i class="fa-solid fa-filter-circle-dollar text-indigo-600"></i> | |
| 漏斗架构师 | |
| </h1> | |
| <p class="text-xs text-gray-500 mt-1">Conversion Funnel Architect</p> | |
| </div> | |
| <div class="flex-1 overflow-y-auto p-6 space-y-6"> | |
| <!-- Global Params --> | |
| <div class="space-y-4"> | |
| <h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider">全局参数</h3> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">流量 (Traffic)</label> | |
| <div class="relative"> | |
| <span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400"><i class="fa-solid fa-users"></i></span> | |
| <input v-model.number="config.traffic" type="number" class="pl-10 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2" placeholder="10000"> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">单次点击成本 (CPC)</label> | |
| <div class="relative"> | |
| <span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400">¥</span> | |
| <input v-model.number="config.cpc" type="number" step="0.1" class="pl-8 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2" placeholder="1.5"> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">产品单价 (Price)</label> | |
| <div class="relative"> | |
| <span class="absolute inset-y-0 left-0 pl-3 flex items-center text-gray-400">¥</span> | |
| <input v-model.number="config.product_price" type="number" class="pl-8 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2" placeholder="99"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="border-t border-gray-100 pt-6"></div> | |
| <!-- Steps Editor --> | |
| <div class="space-y-4"> | |
| <div class="flex justify-between items-center"> | |
| <h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider">漏斗步骤</h3> | |
| <button @click="addStep" class="text-xs bg-indigo-50 text-indigo-600 px-2 py-1 rounded hover:bg-indigo-100 transition"> | |
| <i class="fa-solid fa-plus"></i> 添加 | |
| </button> | |
| </div> | |
| <div v-for="(step, index) in config.steps" :key="index" class="bg-gray-50 p-3 rounded-lg border border-gray-200 group relative"> | |
| <button @click="removeStep(index)" class="absolute top-2 right-2 text-gray-400 hover:text-red-500 opacity-0 group-hover:opacity-100 transition"> | |
| <i class="fa-solid fa-trash"></i> | |
| </button> | |
| <div class="mb-2"> | |
| <label class="text-xs text-gray-500 block mb-1">步骤名称</label> | |
| <input v-model="step.name" type="text" class="w-full text-sm border-gray-300 rounded border p-1 focus:ring-indigo-500 focus:border-indigo-500"> | |
| </div> | |
| <div> | |
| <div class="flex justify-between mb-1"> | |
| <label class="text-xs text-gray-500">转化率</label> | |
| <span class="text-xs font-medium text-indigo-600">${ step.rate }%</span> | |
| </div> | |
| <input v-model.number="step.rate" type="range" min="1" max="100" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="p-6 border-t border-gray-200 bg-gray-50"> | |
| <button @click="simulate" :disabled="loading" class="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition shadow-md font-medium flex justify-center items-center gap-2"> | |
| <i v-if="loading" class="fa-solid fa-circle-notch fa-spin"></i> | |
| <span v-else><i class="fa-solid fa-play"></i> 开始模拟</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="flex-1 flex flex-col h-full overflow-hidden bg-gray-50"> | |
| <!-- Header --> | |
| <header class="bg-white shadow-sm px-8 py-4 flex justify-between items-center z-10"> | |
| <h2 class="text-lg font-medium text-gray-800">模拟仪表盘</h2> | |
| <div class="flex gap-3"> | |
| <input type="file" ref="fileInput" @change="handleFileUpload" class="hidden" accept=".json"> | |
| <button @click="triggerUpload" class="text-gray-600 hover:text-indigo-600 px-3 py-1.5 text-sm border border-gray-300 rounded-md hover:border-indigo-300 transition bg-white"> | |
| <i class="fa-solid fa-upload"></i> 导入配置 | |
| </button> | |
| <button @click="exportConfig" class="text-gray-600 hover:text-indigo-600 px-3 py-1.5 text-sm border border-gray-300 rounded-md hover:border-indigo-300 transition bg-white"> | |
| <i class="fa-solid fa-download"></i> 导出配置 | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Dashboard Content --> | |
| <main class="flex-1 overflow-y-auto p-8"> | |
| <!-- Metrics Cards --> | |
| <div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8"> | |
| <div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="text-sm font-medium text-gray-500">总投入 (Spend)</h3> | |
| <div class="p-2 bg-blue-50 rounded-full text-blue-600"><i class="fa-solid fa-wallet"></i></div> | |
| </div> | |
| <p class="text-2xl font-bold text-gray-800">¥${ formatNumber(metrics.total_spend) }</p> | |
| </div> | |
| <div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="text-sm font-medium text-gray-500">总营收 (Revenue)</h3> | |
| <div class="p-2 bg-green-50 rounded-full text-green-600"><i class="fa-solid fa-sack-dollar"></i></div> | |
| </div> | |
| <p class="text-2xl font-bold text-gray-800">¥${ formatNumber(metrics.total_revenue) }</p> | |
| <p class="text-xs mt-1" :class="metrics.total_revenue > metrics.total_spend ? 'text-green-500' : 'text-red-500'"> | |
| ${ metrics.total_revenue > metrics.total_spend ? '+' : '' }${ formatNumber(metrics.total_revenue - metrics.total_spend) } 净利 | |
| </p> | |
| </div> | |
| <div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="text-sm font-medium text-gray-500">ROI (回报率)</h3> | |
| <div class="p-2 bg-purple-50 rounded-full text-purple-600"><i class="fa-solid fa-chart-line"></i></div> | |
| </div> | |
| <p class="text-2xl font-bold" :class="metrics.roi >= 0 ? 'text-green-600' : 'text-red-600'"> | |
| ${ metrics.roi }% | |
| </p> | |
| </div> | |
| <div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100 hover:shadow-md transition"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="text-sm font-medium text-gray-500">CAC (获客成本)</h3> | |
| <div class="p-2 bg-orange-50 rounded-full text-orange-600"><i class="fa-solid fa-user-tag"></i></div> | |
| </div> | |
| <p class="text-2xl font-bold text-gray-800">¥${ metrics.cac }</p> | |
| </div> | |
| </div> | |
| <!-- Charts Area --> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8"> | |
| <!-- Funnel Chart --> | |
| <div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100"> | |
| <h3 class="text-lg font-semibold text-gray-800 mb-4">漏斗转化视图</h3> | |
| <div id="funnelChart" class="chart-container"></div> | |
| </div> | |
| <!-- Dropoff Chart --> | |
| <div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100"> | |
| <h3 class="text-lg font-semibold text-gray-800 mb-4">流失分析</h3> | |
| <div id="dropoffChart" class="chart-container"></div> | |
| </div> | |
| </div> | |
| <!-- Detailed Table --> | |
| <div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"> | |
| <div class="px-6 py-4 border-b border-gray-100"> | |
| <h3 class="text-lg font-semibold text-gray-800">详细数据表</h3> | |
| </div> | |
| <div class="overflow-x-auto"> | |
| <table class="w-full text-sm text-left text-gray-500"> | |
| <thead class="text-xs text-gray-700 uppercase bg-gray-50"> | |
| <tr> | |
| <th class="px-6 py-3">步骤名称</th> | |
| <th class="px-6 py-3">用户数</th> | |
| <th class="px-6 py-3">转化率</th> | |
| <th class="px-6 py-3">流失用户</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr v-for="(item, index) in funnelData" :key="index" class="bg-white border-b hover:bg-gray-50"> | |
| <td class="px-6 py-4 font-medium text-gray-900">${ item.step_name }</td> | |
| <td class="px-6 py-4">${ formatNumber(item.users) }</td> | |
| <td class="px-6 py-4"> | |
| <span class="px-2 py-1 bg-indigo-50 text-indigo-700 rounded-full text-xs"> | |
| ${ item.conversion_rate }% | |
| </span> | |
| </td> | |
| <td class="px-6 py-4 text-red-500">${ item.dropoff > 0 ? '-' + formatNumber(item.dropoff) : '-' }</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| </div> | |
| <script> | |
| const { createApp, ref, onMounted, nextTick } = Vue; | |
| createApp({ | |
| delimiters: ['${', '}'], | |
| setup() { | |
| const loading = ref(false); | |
| const config = ref({ | |
| traffic: 10000, | |
| cpc: 2.5, | |
| product_price: 199, | |
| steps: [ | |
| { name: '着陆页 (Landing Page)', rate: 40 }, | |
| { name: '注册 (Sign Up)', rate: 25 }, | |
| { name: '试用 (Trial)', rate: 15 }, | |
| { name: '付费 (Purchase)', rate: 20 } | |
| ] | |
| }); | |
| const metrics = ref({ | |
| total_spend: 0, | |
| total_revenue: 0, | |
| roi: 0, | |
| cac: 0, | |
| final_conversions: 0 | |
| }); | |
| const funnelData = ref([]); | |
| let funnelChart = null; | |
| let dropoffChart = null; | |
| const formatNumber = (num) => { | |
| return new Intl.NumberFormat().format(Math.round(num)); | |
| }; | |
| const addStep = () => { | |
| config.value.steps.push({ name: '新步骤', rate: 50 }); | |
| }; | |
| const removeStep = (index) => { | |
| config.value.steps.splice(index, 1); | |
| }; | |
| const initCharts = () => { | |
| const funnelChartDom = document.getElementById('funnelChart'); | |
| const dropoffChartDom = document.getElementById('dropoffChart'); | |
| if (funnelChartDom) funnelChart = echarts.init(funnelChartDom); | |
| if (dropoffChartDom) dropoffChart = echarts.init(dropoffChartDom); | |
| window.addEventListener('resize', () => { | |
| funnelChart && funnelChart.resize(); | |
| dropoffChart && dropoffChart.resize(); | |
| }); | |
| }; | |
| const updateCharts = (data) => { | |
| if (!funnelChart || !dropoffChart) return; | |
| // Funnel Chart | |
| const funnelSeriesData = data.map(item => ({ | |
| value: item.users, | |
| name: item.step_name | |
| })); | |
| const optionFunnel = { | |
| tooltip: { | |
| trigger: 'item', | |
| formatter: "{b} : {c} ({d}%)" | |
| }, | |
| toolbox: { | |
| feature: { | |
| saveAsImage: {} | |
| } | |
| }, | |
| series: [ | |
| { | |
| name: 'Funnel', | |
| type: 'funnel', | |
| left: '10%', | |
| top: 60, | |
| bottom: 60, | |
| width: '80%', | |
| min: 0, | |
| max: data[0].users, // Base on first step | |
| minSize: '0%', | |
| maxSize: '100%', | |
| sort: 'none', | |
| gap: 2, | |
| label: { | |
| show: true, | |
| position: 'inside' | |
| }, | |
| labelLine: { | |
| length: 10, | |
| lineStyle: { | |
| width: 1, | |
| type: 'solid' | |
| } | |
| }, | |
| itemStyle: { | |
| borderColor: '#fff', | |
| borderWidth: 1 | |
| }, | |
| emphasis: { | |
| label: { | |
| fontSize: 20 | |
| } | |
| }, | |
| data: funnelSeriesData | |
| } | |
| ] | |
| }; | |
| funnelChart.setOption(optionFunnel); | |
| // Dropoff Chart (Bar) | |
| const steps = data.map(item => item.step_name); | |
| const dropoffs = data.map(item => item.dropoff); | |
| const optionBar = { | |
| tooltip: { | |
| trigger: 'axis', | |
| axisPointer: { type: 'shadow' } | |
| }, | |
| grid: { | |
| left: '3%', | |
| right: '4%', | |
| bottom: '3%', | |
| containLabel: true | |
| }, | |
| xAxis: { | |
| type: 'category', | |
| data: steps, | |
| axisLabel: { interval: 0, rotate: 30 } | |
| }, | |
| yAxis: { | |
| type: 'value', | |
| name: '流失用户数' | |
| }, | |
| series: [ | |
| { | |
| data: dropoffs, | |
| type: 'bar', | |
| itemStyle: { | |
| color: '#ef4444' | |
| }, | |
| label: { | |
| show: true, | |
| position: 'top' | |
| } | |
| } | |
| ] | |
| }; | |
| dropoffChart.setOption(optionBar); | |
| }; | |
| const simulate = async () => { | |
| loading.value = true; | |
| try { | |
| const response = await fetch('/api/simulate', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(config.value) | |
| }); | |
| const result = await response.json(); | |
| if (result.metrics) { | |
| metrics.value = result.metrics; | |
| funnelData.value = result.funnel_data; | |
| nextTick(() => { | |
| updateCharts(result.funnel_data); | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('Simulation failed:', error); | |
| alert('模拟失败,请检查控制台'); | |
| } finally { | |
| loading.value = false; | |
| } | |
| }; | |
| const exportConfig = () => { | |
| const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(config.value, null, 2)); | |
| const downloadAnchorNode = document.createElement('a'); | |
| downloadAnchorNode.setAttribute("href", dataStr); | |
| downloadAnchorNode.setAttribute("download", "funnel_config.json"); | |
| document.body.appendChild(downloadAnchorNode); | |
| downloadAnchorNode.click(); | |
| downloadAnchorNode.remove(); | |
| }; | |
| const fileInput = ref(null); | |
| const triggerUpload = () => { | |
| fileInput.value.click(); | |
| }; | |
| const handleFileUpload = (event) => { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| try { | |
| const content = JSON.parse(e.target.result); | |
| // Basic validation | |
| if (content.traffic !== undefined && Array.isArray(content.steps)) { | |
| config.value = content; | |
| simulate(); | |
| alert('配置导入成功!'); | |
| } else { | |
| alert('无效的配置文件格式:缺少必要的字段 (traffic, steps)'); | |
| } | |
| } catch (error) { | |
| console.error('Error parsing JSON:', error); | |
| alert('文件解析失败,请确保是有效的JSON文件'); | |
| } | |
| // Reset input | |
| event.target.value = ''; | |
| }; | |
| reader.readAsText(file); | |
| }; | |
| onMounted(() => { | |
| initCharts(); | |
| simulate(); // Initial run | |
| }); | |
| return { | |
| config, | |
| metrics, | |
| funnelData, | |
| loading, | |
| addStep, | |
| removeStep, | |
| simulate, | |
| exportConfig, | |
| triggerUpload, | |
| handleFileUpload, | |
| fileInput, | |
| formatNumber | |
| }; | |
| } | |
| }).mount('#app'); | |
| </script> | |
| </body> | |
| </html> | |