feat(analytics): 员工排行条形图对比+近30天范围——幅度一眼可读/本地停更窗口也可用

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-07 13:43:23 +08:00
parent 1aadc80d6b
commit bbfad520f2
4 changed files with 86 additions and 10 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ import { getJson, requestJson } from './client'
import type { Channel, Page } from './types'
export type { Channel }
export type Scope = 'today' | 'week' | 'month'
export type Scope = 'today' | 'week' | 'month' | 'last30'
// 看板 v2(2026-09-07):overview 渠道参数支持 'all'=全平台(后端译成
// channel=None);列表页(churn/unreplied)仍是单渠道,沿用 types.ts 的 Channel
export type DashboardChannel = 'all' | Channel
+72 -3
View File
@@ -10,6 +10,7 @@
<el-radio-button value="today">今日</el-radio-button>
<el-radio-button value="week">本周</el-radio-button>
<el-radio-button value="month">本月</el-radio-button>
<el-radio-button value="last30">近30天</el-radio-button>
</el-radio-group>
<span v-if="asOfText" class="as-of">数据截至 {{ asOfText }}</span>
</div>
@@ -69,12 +70,21 @@
<div ref="chartEl" class="trend-chart"></div>
</el-card>
<!-- 员工活跃排行:全行展示(~41 人,420px 内滚),按消息量降序(后端排好) -->
<!-- 员工活跃排行:条形图(量级对比一眼可读,2026-09-07「看不到对比」反馈)
+ 明细表(图表的 table-view 孪生)。图取当前指标 TOP 20,表仍全量。 -->
<el-card v-loading="loading" shadow="never" class="rank-card">
<template #header>
<span>员工活跃排行</span>
<span class="rank-scope">按所选时间范围</span>
<div class="trend-header">
<span>员工活跃排行</span>
<el-radio-group v-model="rankMetric" size="small">
<el-radio-button value="msg_total">消息量</el-radio-button>
<el-radio-button value="active_friends">活跃客户</el-radio-button>
<el-radio-button value="call_cnt">通话量</el-radio-button>
</el-radio-group>
<span class="rank-scope">按所选时间范围 · TOP {{ rankBarRows.length }}</span>
</div>
</template>
<div v-if="rankBarRows.length" ref="rankBarEl" class="rank-bar"></div>
<el-table :data="data?.staff_ranking ?? []" max-height="420" size="small">
<el-table-column type="index" label="#" width="48" />
<el-table-column label="员工" min-width="160">
@@ -416,6 +426,61 @@ function draw() {
// post:等 DOM 更新后再画,保证首帧 chartEl 已挂载(趋势图恒 30 天,随 data 重画)
watch([data, trendMetric], draw, { flush: 'post' })
// ── 员工排行条形图:幅度对比的正确图型(横向条形),单系列单色(名义
// 类别不做值渐变——颜色随实体不随排名),条端标值,其余靠轴+表。──
const rankMetric = ref<'msg_total' | 'active_friends' | 'call_cnt'>('msg_total')
const rankBarEl = ref<HTMLElement | null>(null)
const { render: renderRankBar } = useChart(rankBarEl)
const RANK_BAR_TOP = 20
const rankBarRows = computed(() =>
(data.value?.staff_ranking ?? [])
.filter((r) => r[rankMetric.value] > 0)
.slice(0, RANK_BAR_TOP),
)
function drawRankBar() {
const rows = rankBarRows.value
if (!rows.length) return
renderRankBar({
grid: { left: 8, right: 64, top: 8, bottom: 8, containLabel: true },
tooltip: {
trigger: 'item',
valueFormatter: (v: unknown) => compact(Number(v)),
},
xAxis: {
type: 'value',
axisLabel: { formatter: (v: number) => compact(v), color: '#909399' },
splitLine: { lineStyle: { type: 'solid', color: '#f0f1f3' } },
},
yAxis: {
type: 'category',
inverse: true, // 第 1 名在顶部
data: rows.map((r) => r.name),
axisTick: { show: false },
axisLine: { show: false },
axisLabel: { color: '#606266' },
},
series: [
{
type: 'bar',
barWidth: 14, // ≤24px 细条规范
itemStyle: { color: '#409EFF', borderRadius: [0, 4, 4, 0] }, // 数据端 4px 圆角,基线方角
label: {
show: true,
position: 'right',
formatter: (p: { value: unknown }) => compact(Number(p.value)),
color: '#606266',
fontSize: 11,
},
data: rows.map((r) => r[rankMetric.value]),
},
],
})
}
watch([data, rankMetric], drawRankBar, { flush: 'post' })
onMounted(() => {
load()
// health(数据截至)与筛选无关,挂载拉一次即可;失败只影响角标(静默隐藏),
@@ -515,6 +580,10 @@ onMounted(() => {
font-size: 22px;
font-weight: 600;
}
.rank-bar {
height: 480px;
margin-bottom: 8px;
}
.rank-card {
margin-top: 16px;
}