Files
cma-management/frontend/src/views/CustomerDashboard.vue
T

140 lines
4.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>