feat(management): 员工总览页——四视图/服务端排序分页/对比抽屉/CSV口径注释导出
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user