feat(management): 驾驶舱KPI环比(prev_kpis+delta)+异常卡带员工名跳详情+列表进详情带筛选
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,8 @@ export interface SvcTrendPoint { day: number; fr_p50_ms: number | null; fr_p90_m
|
||||
export interface DimSummary { [dim: string]: { name: string; weight: number; scored_count: number; pending_count: number; insufficient_count: number; avg_score: number | null } }
|
||||
export interface AnomalyCard {
|
||||
rule_code: string; severity: 'critical' | 'warning'; employee_user_id: string
|
||||
// dashboard 端点 enrich(批次D,跳员工详情用);员工详情端点不带
|
||||
employee_name?: string
|
||||
title: string; current_value: number; baseline_value: number | null; unit: string
|
||||
sample_size: number; affected_count: number; reason: string
|
||||
drilldown: { type: string; query: { metric_code?: string; user_id?: string } }
|
||||
@@ -34,6 +36,9 @@ export interface AnomalyCard {
|
||||
export interface DashboardResp {
|
||||
scope: MgmtScope; data_as_of: number | null; metric_version: string; config_source: 'db' | 'default'
|
||||
kpis: Kpis
|
||||
// 前窗环比块(批次D):prev 窗同式聚合;可选防御旧缓存响应
|
||||
prev_kpis?: Pick<Kpis, 'active_customers' | 'active_employees' | 'msg_total' | 'cust_threads'>
|
||||
msg_delta?: number | null // (msg_total-prev)/prev,prev=0 → null;客户端三卡自算,此键备用
|
||||
communication_trend: CommTrendPoint[]
|
||||
service_trend: SvcTrendPoint[]
|
||||
lifecycle: Kpis['lifecycle']
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
<el-table-column label="异常" min-width="240">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-title">{{ row.title }}</div>
|
||||
<div v-if="showEmployee" class="cell-emp">
|
||||
<el-link
|
||||
type="primary"
|
||||
:underline="false"
|
||||
@click.stop="goEmployee(row)"
|
||||
>{{ row.employee_name || row.employee_user_id.slice(0, 8) }}</el-link>
|
||||
</div>
|
||||
<div class="cell-sub">{{ row.reason }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -43,16 +50,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 异常榜(批次C 驾驶舱):后端 AnomalyCard[] 平铺表——级别 tag/标题+原因
|
||||
// 双行/单位感知当前值+基线/样本/影响(≥50 红显)/建议动作。行 click 打开
|
||||
// 异常榜(批次C 驾驶舱):后端 AnomalyCard[] 平铺表——级别 tag/标题+员工行
|
||||
// (showEmployee,批次D:employee_name 链接→员工详情,.stop 防触发行下钻)
|
||||
// +原因/单位感知当前值+基线/样本/影响(≥50 红显)/建议动作。行 click 打开
|
||||
// 下钻抽屉(emit drill,抽屉自治加载);critical 整行淡红底提示优先处置。
|
||||
// 纯展示+事件,不取数。
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { formatMsDuration, formatRatio } from '@/utils/format'
|
||||
import type { AnomalyCard } from '@/api/management'
|
||||
|
||||
// showEmployee(默认 true):D3 驾驶舱异常榜将在标题下加员工行(employee_name
|
||||
// 链接→员工详情);员工详情页复用本组件时传 false 免自链接——prop 先行落位
|
||||
// (批次D2),员工行的渲染随 D3 接上。
|
||||
// showEmployee(默认 true):驾驶舱异常榜显示员工行;员工详情页复用本组件
|
||||
// 传 false(自指链接无意义)
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
anomalies: AnomalyCard[]
|
||||
@@ -66,6 +74,18 @@ const emit = defineEmits<{
|
||||
(e: 'drill', card: AnomalyCard): void
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 员工链接:带当前 URL query(channel/start/end/compare 随行,与列表
|
||||
// goDetail 带窗进详情同语义);detail 页对白名单外 query 键不敏感
|
||||
function goEmployee(row: AnomalyCard): void {
|
||||
router.push({
|
||||
path: `/management/employees/${row.employee_user_id}`,
|
||||
query: route.query,
|
||||
})
|
||||
}
|
||||
|
||||
// 单位感知格式化:ms→时长,0-1→比率,其余千分位(与指标口径字典 unit 对齐)
|
||||
function formatValue(v: number, unit: string): string {
|
||||
if (unit === 'ms') return formatMsDuration(v)
|
||||
@@ -96,6 +116,14 @@ function onRowClick(row: AnomalyCard): void {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.cell-emp {
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.cell-emp .el-link {
|
||||
font-size: 12px;
|
||||
}
|
||||
.cell-sub {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
@@ -4,14 +4,18 @@
|
||||
<div class="kpi-value" :class="{ danger }">
|
||||
{{ valueText }}<span v-if="unit && value !== null" class="kpi-unit">{{ unit }}</span>
|
||||
</div>
|
||||
<div v-if="deltaText" class="kpi-delta" :class="deltaClass">
|
||||
环比{{ deltaText }}
|
||||
</div>
|
||||
<div v-if="hint" class="kpi-hint">{{ hint }}</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 经营管理 KPI 卡:label 上小灰字 + 主值大号字(千分位/unit 后缀/danger 红)
|
||||
// + hint 下小灰字。to 存在时整卡可点(hover 手型)跳转——纯展示+导航,
|
||||
// 不取数。null 值语义(无样本)显 `--`,由父级决定传 null。
|
||||
// + 环比行(批次D,delta prop)+ hint 下小灰字。to 存在时整卡可点
|
||||
// (hover 手型)跳转——纯展示+导航,不取数。null 值语义(无样本)显 `--`,
|
||||
// 由父级决定传 null。
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -22,12 +26,26 @@ const props = defineProps<{
|
||||
hint?: string
|
||||
to?: string
|
||||
danger?: boolean
|
||||
// 较前窗环比 (cur-prev)/prev:null/undefined/NaN(前窗缺数据)不显示。
|
||||
// 只支持升好语义(升绿↓红)——本批接入三卡(活跃客户/活跃员工/消息量)
|
||||
// 均为越高越好;将来若有降好指标(如首响时长)需先加方向语义位
|
||||
delta?: number | null
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const valueText = computed(() => (props.value === null ? '--' : props.value.toLocaleString()))
|
||||
|
||||
const deltaText = computed(() => {
|
||||
if (props.delta == null || !Number.isFinite(props.delta)) return null
|
||||
const arrow = props.delta > 0 ? '↑' : props.delta < 0 ? '↓' : '→'
|
||||
return `${arrow}${(Math.abs(props.delta) * 100).toFixed(1)}%`
|
||||
})
|
||||
const deltaClass = computed(() =>
|
||||
props.delta == null || !Number.isFinite(props.delta)
|
||||
? ''
|
||||
: props.delta > 0 ? 'up' : props.delta < 0 ? 'down' : 'flat')
|
||||
|
||||
function onClick(): void {
|
||||
if (props.to) router.push(props.to)
|
||||
}
|
||||
@@ -56,6 +74,19 @@ function onClick(): void {
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-left: 2px;
|
||||
}
|
||||
.kpi-delta {
|
||||
font-size: 12px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.kpi-delta.up {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
.kpi-delta.down {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
.kpi-delta.flat {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.kpi-hint {
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
<el-table-column type="selection" width="42" reserve-selection />
|
||||
<el-table-column label="姓名" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<!-- 详情路由批次D后续任务上线;当前点击走通配回 /customers(记录在案) -->
|
||||
<!-- 带窗进详情:goDetail 随行 channel/start/end/compare(D3) -->
|
||||
<el-link type="primary" underline="never" @click="goDetail(row)">
|
||||
{{ row.name || row.user_id }}
|
||||
</el-link>
|
||||
@@ -535,8 +535,16 @@ function gradeType(g: string | null): 'success' | 'primary' | 'warning' | 'dange
|
||||
return (g && GRADE_TYPES[g]) || 'info'
|
||||
}
|
||||
|
||||
// 带窗进详情(D3):channel/start/end/compare 随行——列表口径延续到详情页;
|
||||
// view/keyword/status/sort_by/sort_order/page 是列表态,不带(详情页只认
|
||||
// channel/start/end/compare/section,其余键即使带上也被白名单忽略)
|
||||
function goDetail(row: EmployeeRow): void {
|
||||
router.push(`/management/employees/${row.user_id}`)
|
||||
const query: Record<string, string> = {}
|
||||
for (const key of ['channel', 'start', 'end', 'compare'] as const) {
|
||||
const v = route.query[key]
|
||||
if (typeof v === 'string' && v !== '') query[key] = v
|
||||
}
|
||||
router.push({ name: 'management-employees-detail', params: { userId: row.user_id }, query })
|
||||
}
|
||||
|
||||
// ── 多选 → 对比抽屉(reserve-selection 跨页/重查保留;行是勾选时快照) ──
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
<ManagementKpiCard
|
||||
label="活跃客户"
|
||||
:value="kpis?.active_customers ?? null"
|
||||
:delta="deltaOf('active_customers')"
|
||||
unit="人"
|
||||
:hint="threadsHint"
|
||||
/>
|
||||
@@ -52,6 +53,7 @@
|
||||
<ManagementKpiCard
|
||||
label="活跃员工"
|
||||
:value="kpis?.active_employees ?? null"
|
||||
:delta="deltaOf('active_employees')"
|
||||
unit="人"
|
||||
hint="期内有数据天数>0 口径"
|
||||
/>
|
||||
@@ -65,6 +67,7 @@
|
||||
<ManagementKpiCard
|
||||
label="消息量"
|
||||
:value="kpis?.msg_total ?? null"
|
||||
:delta="deltaOf('msg_total')"
|
||||
unit="条"
|
||||
hint="1:1口径·与数据看板口径不同"
|
||||
/>
|
||||
@@ -209,6 +212,16 @@ watch([channel, start, end, compare], load, { immediate: true })
|
||||
const kpis = computed(() => resp.value?.kpis ?? null)
|
||||
const actions = computed(() => resp.value?.action_summary ?? null)
|
||||
|
||||
// 三 KPI 卡环比(批次D):客户端统一按 (cur-prev)/prev 自 prev_kpis 计算
|
||||
// (数据同载荷都在;后端 msg_delta 备用未用)。prev_kpis 缺失(旧缓存
|
||||
// 响应)或 prev=0(前窗无数据)→ null 不显示
|
||||
function deltaOf(key: 'active_customers' | 'active_employees' | 'msg_total'): number | null {
|
||||
const k = resp.value?.kpis
|
||||
const p = resp.value?.prev_kpis
|
||||
if (!k || !p || !p[key]) return null
|
||||
return (k[key] - p[key]) / p[key]
|
||||
}
|
||||
|
||||
// 活跃客户辅助行:串/被回(口径与 KPI 同载荷,管理域跨日精确串)
|
||||
const threadsHint = computed(() => {
|
||||
const k = kpis.value
|
||||
|
||||
Reference in New Issue
Block a user