feat(management): 员工总览页——四视图/服务端排序分页/对比抽屉/CSV口径注释导出

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-10 11:44:03 +08:00
parent 602bee8fdf
commit b72e957da2
5 changed files with 909 additions and 2 deletions
@@ -0,0 +1,169 @@
<template>
<el-drawer
:model-value="modelValue"
title="员工对比"
size="620px"
@update:model-value="emit('update:modelValue', $event)"
>
<!-- v1 §6.6:对比只呈现可验证差异,不产生优劣结论(管理者需被提醒) -->
<el-alert
type="info"
:closable="false"
title="同口径同时窗对比,仅呈现可验证差异,不产生优劣结论"
class="cmp-alert"
/>
<!-- ① 维度对比:三可用维度,每人一行横向 bar(0-100;无样本灰「--」) -->
<div v-for="dim in DIMS" :key="dim.key" class="dim-block">
<div class="dim-title">{{ dim.label }}</div>
<div v-for="r in rows" :key="r.user_id" class="dim-row">
<span class="dim-name" :title="r.user_id">{{ displayName(r) }}</span>
<template v-if="dimScore(r, dim.key) !== null">
<div class="dim-track">
<div class="dim-bar" :style="{ width: `${dimScore(r, dim.key)}%` }" />
</div>
<span class="dim-score">{{ Math.round(dimScore(r, dim.key) ?? 0) }}</span>
</template>
<span v-else class="dim-na">--</span>
</div>
</div>
<!-- ② 关键指标表:行=指标,列=每人 -->
<el-table :data="metricRows" size="small" border class="cmp-table">
<el-table-column prop="label" label="指标" min-width="96" />
<el-table-column
v-for="r in rows"
:key="r.user_id"
align="right"
min-width="88"
>
<template #header>
<span :title="r.user_id">{{ displayName(r) }}</span>
</template>
<template #default="{ row }">{{ row.values[r.user_id] }}</template>
</el-table-column>
</el-table>
</el-drawer>
</template>
<script setup lang="ts">
// 员工对比抽屉(批次D):总览页勾选 2-5 人后并排看维度分与关键指标。
// 只读展示——数据来自已选行快照(EmployeeRow),不二次请求;口径/时窗与
// 总览页完全一致(同载荷),故顶部钉 v1 §6.6 提示:仅呈现可验证差异。
import { computed } from 'vue'
import type { EmployeeRow } from '@/api/management'
import { formatMsDuration, formatRatio } from '@/utils/format'
const props = defineProps<{
modelValue: boolean
/** 已勾选行(2-5,由父级按钮门槛保证);行是勾选时的快照,翻页/重查不影响 */
rows: EmployeeRow[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', v: boolean): void
}>()
// 一期可用三维度(action/risk pending_data 不参与)
const DIMS = [
{ key: 'customer', label: '客户经营' },
{ key: 'service', label: '服务质量' },
{ key: 'workload', label: '工作负载' },
] as const
function displayName(r: EmployeeRow): string {
return r.name || r.user_id.slice(0, 8)
}
function dimScore(r: EmployeeRow, key: string): number | null {
return r.score.dimensions[key]?.score ?? null
}
// metrics 取数值键(user_id 透传键是字符串,typeof 守卫掉)
function num(r: EmployeeRow, key: string): number | null {
const v = r.metrics[key]
return typeof v === 'number' ? v : null
}
function ratio(numV: number | null, den: number | null): string {
if (!den) return '--' // 分母 0/null → 无率(严格区分 0%)
return formatRatio(numV == null ? null : numV / den)
}
interface CmpMetricRow {
label: string
values: Record<string, string>
}
const metricRows = computed<CmpMetricRow[]>(() => {
const mk = (label: string, fmt: (r: EmployeeRow) => string): CmpMetricRow => {
const values: Record<string, string> = {}
for (const r of props.rows) values[r.user_id] = fmt(r)
return { label, values }
}
return [
mk('活跃客户', (r) => String(num(r, 'active_friends') ?? '--')),
mk('消息量', (r) => String((num(r, 'msg_sent') ?? 0) + (num(r, 'msg_recv') ?? 0))),
mk('首响P90', (r) => formatMsDuration(num(r, 'fr_p90_ms'))),
mk('2h回复率', (r) => ratio(num(r, 'reply_2h_count'), num(r, 'cust_threads'))),
mk('异常数', (r) => String(r.anomaly_count)),
]
})
</script>
<style scoped>
.cmp-alert {
margin-bottom: 16px;
}
.dim-block {
margin-bottom: 16px;
}
.dim-title {
font-size: 13px;
font-weight: 600;
color: var(--el-text-color-primary);
margin-bottom: 6px;
}
.dim-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.dim-name {
width: 88px;
flex: none;
font-size: 13px;
color: var(--el-text-color-regular);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dim-track {
flex: 1;
height: 12px;
border-radius: 6px;
background: var(--el-fill-color-light);
overflow: hidden;
}
.dim-bar {
height: 100%;
border-radius: 6px;
background: var(--el-color-primary);
}
.dim-score {
width: 28px;
flex: none;
text-align: right;
font-size: 12px;
color: var(--el-text-color-regular);
}
.dim-na {
flex: 1;
font-size: 12px;
color: var(--el-text-color-placeholder);
}
.cmp-table {
margin-top: 4px;
}
</style>