140 lines
4.9 KiB
Vue
140 lines
4.9 KiB
Vue
<template>
|
||
<div class="customer-dashboard">
|
||
<div class="page-header">
|
||
<h3 class="page-title">客户维度KPI看板</h3>
|
||
</div>
|
||
<KPIListView
|
||
ref="kpiListRef"
|
||
:dimension="'customer'"
|
||
:title="'客户维度KPI看板'"
|
||
:icon="'👥'"
|
||
:stat-labels="{ total: '客户指标总数', green: '达标', yellow: '预警', red: '未达标', noData: '无数据' }"
|
||
:templates="CUSTOMER_TEMPLATES"
|
||
:has-categories="false"
|
||
:show-frequency="true"
|
||
:name-placeholder="'客户满意度、渠道覆盖率'"
|
||
:unit-options="['%', '元', '家', '分', '次']"
|
||
form-title-create="新建客户KPI"
|
||
form-title-edit="编辑客户KPI"
|
||
/>
|
||
|
||
<!-- 趋势分析(客户维度特有) -->
|
||
<el-card shadow="never" class="section-card" v-if="trendData.length > 0">
|
||
<template #header>
|
||
<div class="card-header-flex">
|
||
<span>📈 关键指标趋势</span>
|
||
<el-radio-group v-model="trendType" size="small" @change="loadTrend">
|
||
<el-radio-button value="monthly">月度</el-radio-button>
|
||
<el-radio-button value="quarterly">季度</el-radio-button>
|
||
</el-radio-group>
|
||
</div>
|
||
</template>
|
||
<div v-if="trendLoading" style="padding:40px;text-align:center;">
|
||
<el-skeleton :rows="4" animated />
|
||
</div>
|
||
<div v-else ref="trendChartRef" style="width:100%;height:320px;"></div>
|
||
</el-card>
|
||
|
||
<el-empty v-if="!trendLoading && trendData.length === 0" description="暂无趋势数据" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||
import KPIListView from '../components/KPIListView.vue'
|
||
import api from '../api/index'
|
||
|
||
// ── 客户维度预设模板 ──
|
||
const CUSTOMER_TEMPLATES = [
|
||
{ name: '客户满意度', unit: '%', targetValue: 90 },
|
||
{ name: '客户留存率', unit: '%', targetValue: 85 },
|
||
{ name: '新客户增长率', unit: '%', targetValue: 20 },
|
||
{ name: '客户渠补率', unit: '%', targetValue: 95 },
|
||
{ name: '客户投诉率', unit: '%', targetValue: 5 },
|
||
{ name: '客户平均贡献值', unit: '元', targetValue: 10000 },
|
||
{ name: '客户转化率', unit: '%', targetValue: 30 },
|
||
{ name: '客户活跃度', unit: '%', targetValue: 70 },
|
||
{ name: 'NPS净推荐值', unit: '分', targetValue: 60 },
|
||
]
|
||
|
||
// ── 趋势分析 ──
|
||
const kpiListRef = ref<InstanceType<typeof KPIListView> | null>(null)
|
||
const trendType = ref('monthly')
|
||
const trendLoading = ref(false)
|
||
const trendData = ref<any[]>([])
|
||
const trendChartRef = ref<HTMLElement | null>(null)
|
||
let chartInstance: any = null
|
||
|
||
async function loadTrend() {
|
||
// 从 KPIListView 内部获取当前 kpis
|
||
// 由于 KPIListView 内部数据不暴露,直接通过 API 请求客户维度的前5个KPI
|
||
trendLoading.value = true
|
||
try {
|
||
const r: any = await api.get('/kpis', { params: { dimension: 'customer', page_size: 5 } })
|
||
const kpis = (r.data || []).slice(0, 5)
|
||
const kpiIds = kpis.map((k: any) => k.id).filter(Boolean)
|
||
if (kpiIds.length === 0) { trendLoading.value = false; return }
|
||
const trendRes: any = await api.post('/dashboard/trend-analysis', {
|
||
kpi_ids: kpiIds,
|
||
dimension: 'customer',
|
||
period_type: trendType.value,
|
||
})
|
||
trendData.value = trendRes.data || []
|
||
trendLoading.value = false
|
||
nextTick(() => renderChart())
|
||
} catch {
|
||
trendLoading.value = false
|
||
}
|
||
}
|
||
|
||
function renderChart() {
|
||
if (!trendChartRef.value) return
|
||
try {
|
||
const chart = (window as any).echarts?.init(trendChartRef.value)
|
||
if (chart) {
|
||
chartInstance = chart
|
||
const series = trendData.value.slice(0, 5).map((d: any) => ({
|
||
name: d.kpi_name,
|
||
type: 'line',
|
||
smooth: true,
|
||
data: (d.period_values || []).map((pv: any) => pv.value),
|
||
}))
|
||
const months = trendData.value[0]?.period_values?.map((pv: any) => pv.period) || []
|
||
chart.setOption({
|
||
tooltip: { trigger: 'axis' },
|
||
legend: { data: trendData.value.slice(0, 5).map((d: any) => d.kpi_name) },
|
||
grid: { left: 60, right: 20, bottom: 30, top: 40 },
|
||
xAxis: { type: 'category', data: months },
|
||
yAxis: { type: 'value' },
|
||
series,
|
||
})
|
||
}
|
||
} catch { /* echarts not loaded */ }
|
||
}
|
||
|
||
onMounted(() => {
|
||
// 等待 KPIListView 加载完成后加载趋势
|
||
setTimeout(() => loadTrend(), 500)
|
||
// 尝试加载 ECharts
|
||
if (!(window as any).echarts) {
|
||
const script = document.createElement('script')
|
||
script.src = '/assets/echarts.min.js'
|
||
script.onload = () => { nextTick(loadTrend) }
|
||
document.head.appendChild(script)
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
if (chartInstance) {
|
||
chartInstance.dispose()
|
||
chartInstance = null
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.customer-dashboard { padding: 0; }
|
||
.section-card { margin-bottom: 16px; margin-top: 16px; }
|
||
.card-header-flex { display: flex; justify-content: space-between; align-items: center; }
|
||
</style>
|