143 行
3.8 KiB
Vue
143 行
3.8 KiB
Vue
<template>
|
|
<div>
|
|
<h2 style="margin-bottom: 24px">漏斗分析</h2>
|
|
|
|
<!-- App selector bar -->
|
|
<div class="app-selector-bar">
|
|
<span class="selector-label">选择应用</span>
|
|
<el-select
|
|
:model-value="appKey"
|
|
placeholder="请选择应用"
|
|
style="width:220px"
|
|
:loading="loadingApps"
|
|
@change="setApp"
|
|
>
|
|
<el-option v-for="a in apps" :key="a.appKey" :label="a.name" :value="a.appKey" />
|
|
</el-select>
|
|
</div>
|
|
<el-empty v-if="!appKey" description="请选择一个应用" style="margin-top:80px" />
|
|
<template v-else>
|
|
|
|
<el-card style="margin-bottom: 16px">
|
|
<template #header>配置漏斗步骤</template>
|
|
<div class="funnel-builder">
|
|
<div v-for="(step, idx) in steps" :key="idx" class="funnel-step-row">
|
|
<span class="step-index">步骤 {{ idx + 1 }}</span>
|
|
<el-input v-model="steps[idx]" placeholder="事件名" style="width: 280px" />
|
|
<el-button v-if="steps.length > 1" type="danger" text @click="removeStep(idx)">删除</el-button>
|
|
</div>
|
|
<div style="margin-top: 12px; display: flex; gap: 12px">
|
|
<el-button @click="addStep">添加步骤</el-button>
|
|
<el-button type="primary" :loading="loading" @click="analyze">分析</el-button>
|
|
</div>
|
|
</div>
|
|
</el-card>
|
|
|
|
<el-card v-if="funnelData.length">
|
|
<template #header>漏斗结果</template>
|
|
<div class="funnel-result">
|
|
<div v-for="(step, idx) in funnelData" :key="idx" class="funnel-item">
|
|
<div class="funnel-step-info">
|
|
<div class="funnel-step-name">{{ step.eventName }}</div>
|
|
<div class="funnel-step-count">{{ step.count }} 人</div>
|
|
</div>
|
|
<el-progress
|
|
:percentage="step.conversionRate"
|
|
:stroke-width="24"
|
|
:color="funnelColor(idx)"
|
|
:format="(p: number) => p.toFixed(1) + '%'"
|
|
/>
|
|
<div v-if="idx < funnelData.length - 1" class="funnel-drop">
|
|
转化 {{ step.conversionRate.toFixed(1) }}% ↓
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</el-card>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from 'vue'
|
|
import { bugCollectApi, type BugCollectFunnelStep } from '@/api/bugcollect'
|
|
import { useBugCollectApp } from '@/composables/useBugCollectApp'
|
|
|
|
const { apps, loadingApps, appKey, setApp } = useBugCollectApp()
|
|
|
|
const steps = ref(['', ''])
|
|
const funnelData = ref<BugCollectFunnelStep[]>([])
|
|
const loading = ref(false)
|
|
|
|
function addStep() {
|
|
steps.value.push('')
|
|
}
|
|
|
|
function removeStep(idx: number) {
|
|
steps.value.splice(idx, 1)
|
|
}
|
|
|
|
function funnelColor(idx: number) {
|
|
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399']
|
|
return colors[idx % colors.length]
|
|
}
|
|
|
|
async function analyze() {
|
|
const validSteps = steps.value.filter((s) => s.trim())
|
|
if (validSteps.length < 2) return
|
|
loading.value = true
|
|
try {
|
|
const res = await bugCollectApi.funnel(appKey.value, validSteps)
|
|
funnelData.value = res.data.data
|
|
} catch {
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.app-selector-bar { display:flex; align-items:center; gap:12px; margin-bottom:20px; }
|
|
.selector-label { font-size:14px; color:#606266; }
|
|
.funnel-builder {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.funnel-step-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.step-index {
|
|
width: 60px;
|
|
font-size: 13px;
|
|
color: #666;
|
|
flex-shrink: 0;
|
|
}
|
|
.funnel-result {
|
|
max-width: 600px;
|
|
}
|
|
.funnel-item {
|
|
margin-bottom: 20px;
|
|
}
|
|
.funnel-step-info {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 6px;
|
|
}
|
|
.funnel-step-name {
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
}
|
|
.funnel-step-count {
|
|
font-size: 14px;
|
|
color: #666;
|
|
}
|
|
.funnel-drop {
|
|
text-align: center;
|
|
font-size: 12px;
|
|
color: #999;
|
|
margin-top: 4px;
|
|
}
|
|
</style>
|