fix(analytics): 趋势图可读性——摘要行/轴刻度稀疏与大数万化/实线细网格/峰值标注/均值参考线
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -55,11 +55,15 @@
|
||||
|
||||
<el-card v-loading="loading" shadow="never" class="trend-card">
|
||||
<template #header>
|
||||
<el-radio-group v-model="trendMetric" size="small">
|
||||
<el-radio-button value="msg">消息量</el-radio-button>
|
||||
<el-radio-button value="active_customers">活跃客户</el-radio-button>
|
||||
<el-radio-button value="call_cnt">通话量</el-radio-button>
|
||||
</el-radio-group>
|
||||
<div class="trend-header">
|
||||
<el-radio-group v-model="trendMetric" size="small">
|
||||
<el-radio-button value="msg">消息量</el-radio-button>
|
||||
<el-radio-button value="active_customers">活跃客户</el-radio-button>
|
||||
<el-radio-button value="call_cnt">通话量</el-radio-button>
|
||||
</el-radio-group>
|
||||
<!-- 数字摘要行:图之前先给一眼可读的合计/日均/峰值 -->
|
||||
<span v-if="trendSummary" class="trend-summary">近30天 {{ trendSummary }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="chartEl" class="trend-chart"></div>
|
||||
</el-card>
|
||||
@@ -208,6 +212,10 @@ const anomalyView = computed<AnomalyView | null>(() => {
|
||||
|
||||
const fmt = (n: number | undefined | null) => (n ?? 0).toLocaleString()
|
||||
|
||||
// 趋势摘要:先给数字再看图(合计/日均/峰值及日期)
|
||||
const trendVals = (): number[] =>
|
||||
(data.value?.trend ?? []).map((p) => p[trendMetric.value] as number)
|
||||
|
||||
// 预警行动入口:计数来自 overview 载荷 actions(0 灰显仍可点,直达处置页)
|
||||
const actionCards = computed(() => {
|
||||
const a = data.value?.actions
|
||||
@@ -239,24 +247,90 @@ const cardList = computed<CardItem[]>(() => {
|
||||
]
|
||||
})
|
||||
|
||||
// 大数压缩:轴刻度/标注统一 1.1万 口径(裸 11254 难读)
|
||||
const compact = (n: number) => (n >= 10000 ? `${(n / 10000).toFixed(1)}万` : n.toLocaleString())
|
||||
|
||||
// 趋势摘要:先给数字再看图(合计/日均/峰值及日期)
|
||||
const trendSummary = computed(() => {
|
||||
const t = data.value?.trend
|
||||
if (!t || !t.length) return ''
|
||||
const vals = trendVals()
|
||||
const total = vals.reduce((a, b) => a + b, 0)
|
||||
const max = Math.max(...vals)
|
||||
const maxDay = t[vals.indexOf(max)].day
|
||||
const dstr = `${String(maxDay).slice(4, 6)}-${String(maxDay).slice(6)}`
|
||||
return `合计 ${total.toLocaleString()} · 日均 ${Math.round(total / vals.length).toLocaleString()} · 峰值 ${max.toLocaleString()}(${dstr})`
|
||||
})
|
||||
|
||||
function draw() {
|
||||
const t = data.value?.trend
|
||||
if (!t) return
|
||||
if (!t || !t.length) return
|
||||
const labels = t.map((p) => `${String(p.day).slice(4, 6)}-${String(p.day).slice(6)}`)
|
||||
const vals = trendVals()
|
||||
// X 轴 ~6 个刻度(30 个日期全排会挤成一串不可读)
|
||||
const interval = Math.max(0, Math.ceil(labels.length / 6) - 1)
|
||||
const maxIdx = vals.indexOf(Math.max(...vals))
|
||||
const avg = Math.round(vals.reduce((a, b) => a + b, 0) / vals.length)
|
||||
render({
|
||||
grid: { left: 48, right: 16, top: 24, bottom: 28 },
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: 56, right: 56, top: 28, bottom: 28 },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
valueFormatter: (v: unknown) => compact(Number(v)),
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
// day 是 yyyymmdd int(20260904 → "09-04")
|
||||
data: t.map((p) => `${String(p.day).slice(4, 6)}-${String(p.day).slice(6)}`),
|
||||
data: labels,
|
||||
axisLabel: { interval, hideOverlap: true, color: '#909399' },
|
||||
axisLine: { lineStyle: { color: '#dcdfe6' } },
|
||||
axisTick: { show: false },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: { formatter: (v: number) => compact(v), color: '#909399' },
|
||||
// 实线细网格(虚线网格是可视化反模式——虚线该留给参考线)
|
||||
splitLine: { lineStyle: { type: 'solid', color: '#f0f1f3' } },
|
||||
},
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
areaStyle: { opacity: 0.12 },
|
||||
data: t.map((p) => p[trendMetric.value]),
|
||||
lineStyle: { width: 2 },
|
||||
areaStyle: { opacity: 0.1 },
|
||||
data: vals,
|
||||
showSymbol: false,
|
||||
// 选择性标注:终点一个数(其余靠轴+tooltip,逐点标数是反模式)
|
||||
endLabel: {
|
||||
show: true,
|
||||
formatter: () => compact(vals[vals.length - 1]),
|
||||
color: '#606266',
|
||||
fontSize: 12,
|
||||
},
|
||||
// 峰值:小圆点+2px 表面描边环(默认大水滴太响);标注峰值日的数
|
||||
markPoint: {
|
||||
symbol: 'circle',
|
||||
symbolSize: 9,
|
||||
itemStyle: { borderColor: '#fff', borderWidth: 2 },
|
||||
label: {
|
||||
formatter: () => compact(vals[maxIdx]),
|
||||
fontSize: 11,
|
||||
color: '#606266',
|
||||
offset: [0, -12],
|
||||
},
|
||||
data: [{ coord: [maxIdx, vals[maxIdx]] }],
|
||||
},
|
||||
// 均值参考线:虚线在此语义正确(参考阈值)
|
||||
markLine: {
|
||||
silent: true,
|
||||
symbol: 'none',
|
||||
lineStyle: { type: 'dashed', color: '#c0c4cc', width: 1 },
|
||||
label: {
|
||||
formatter: `均值 ${compact(avg)}`,
|
||||
position: 'insideEndTop',
|
||||
fontSize: 11,
|
||||
color: '#909399',
|
||||
},
|
||||
data: [{ type: 'average', name: '均值' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -324,6 +398,16 @@ onMounted(() => {
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.trend-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.trend-summary {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.trend-chart {
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user