diff --git a/yunkes/src/components/management/EmployeeCompareDrawer.vue b/yunkes/src/components/management/EmployeeCompareDrawer.vue new file mode 100644 index 0000000..6dbe89e --- /dev/null +++ b/yunkes/src/components/management/EmployeeCompareDrawer.vue @@ -0,0 +1,169 @@ + + + + + + + + {{ dim.label }} + + {{ displayName(r) }} + + + + + {{ Math.round(dimScore(r, dim.key) ?? 0) }} + + -- + + + + + + + + + {{ displayName(r) }} + + {{ row.values[r.user_id] }} + + + + + + + + diff --git a/yunkes/src/layouts/MainLayout.vue b/yunkes/src/layouts/MainLayout.vue index f6dd5f4..6f1c496 100644 --- a/yunkes/src/layouts/MainLayout.vue +++ b/yunkes/src/layouts/MainLayout.vue @@ -107,6 +107,9 @@ function logout(): void { 管理驾驶舱 + + 员工分析 + diff --git a/yunkes/src/router/index.ts b/yunkes/src/router/index.ts index 0befb89..27888d6 100644 --- a/yunkes/src/router/index.ts +++ b/yunkes/src/router/index.ts @@ -99,6 +99,14 @@ const routes: RouteRecordRaw[] = [ component: () => import('@/views/management/ManagementDashboardView.vue'), meta: { title: '管理驾驶舱' }, }, + { + // 员工分析(批次D):四视图总览;详情路由 /management/employees/{user_id} + // 由批次D后续任务补挂(D2) + path: '/management/employees', + name: 'management-employees', + component: () => import('@/views/management/EmployeeAnalyticsView.vue'), + meta: { title: '员工分析' }, + }, { path: '/chat/:channel/:ownerId/:talkerId?', name: 'chat', diff --git a/yunkes/src/utils/csvExport.ts b/yunkes/src/utils/csvExport.ts index 8bc92f0..434ce4f 100644 --- a/yunkes/src/utils/csvExport.ts +++ b/yunkes/src/utils/csvExport.ts @@ -10,6 +10,9 @@ * ], * filename: '微信客户列表', * }) + * + * Optional `preamble: string[]` writes comment lines (口径/窗口注释等) before + * the header — each line occupies the first column of its own row. */ export interface CsvColumn { @@ -30,6 +33,12 @@ export interface ExportOptions { pageSize?: number /** Max rows to export (safety cap, default 10000) */ maxRows?: number + /** + * Optional comment lines written before the header row (batch D: management + * employee CSV carries caliber/window notes so the export is self-describing + * outside the app). Each line becomes a single-cell row in the first column. + */ + preamble?: string[] } function escapeCsv(value: string | number | null | undefined): string { @@ -43,7 +52,7 @@ function escapeCsv(value: string | number | null | undefined): string { } export async function exportListToCsv(options: ExportOptions): Promise<{ rows: number; filename: string }> { - const { fetchPage, columns, filename, pageSize = 100, maxRows = 10000 } = options + const { fetchPage, columns, filename, pageSize = 100, maxRows = 10000, preamble } = options // Fetch all pages const allRows: T[] = [] @@ -68,7 +77,7 @@ export async function exportListToCsv(options: ExportOptions): Promise<{ r return escapeCsv(val as string | number | null | undefined) }).join(',') ) - const csv = '' + header + '\n' + lines.join('\n') // BOM for Excel Chinese support + const csv = '' + [...(preamble ?? []).map((p) => escapeCsv(p)), header, ...lines].join('\n') // BOM for Excel Chinese support // Trigger download const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }) diff --git a/yunkes/src/views/management/EmployeeAnalyticsView.vue b/yunkes/src/views/management/EmployeeAnalyticsView.vue new file mode 100644 index 0000000..3bdad1f --- /dev/null +++ b/yunkes/src/views/management/EmployeeAnalyticsView.vue @@ -0,0 +1,718 @@ + + + + + + + + + + + + + + + + + + + + + + + 管理概览 + 工作负载 + 服务质量 + 客户经营 + + + 对比已选 ({{ selectedRows.length }}) + 导出CSV + + + + + + + + {{ loadError }} + 重试 + + + + + + + + + + + {{ row.name || row.user_id }} + + {{ row.user_id }} + + + + + 在职 + + + 在职 + 离职 + -- + + + + + + + + 综合分 + + + + + + + + -- + + + {{ Math.round(row.score.total_score ?? 0) }} + {{ row.score.grade }} + 试 + + + + + + {{ row.anomaly_count }} + 权重 {{ row.anomaly_weight }} + + + + + 活跃客户 + + + + + + {{ fmtInt(num(row, 'active_friends')) }} + + + + 消息量 + + {{ fmtInt(msgTotal(row)) }} + + + + 串/被回 + + + {{ fmtInt(num(row, 'cust_threads')) }} / {{ fmtInt(num(row, 'threads_answered')) }} + + + + + 客户经营 + + + {{ dimScoreText(row, 'customer') }} + + + + + 服务质量 + + + {{ dimScoreText(row, 'service') }} + + + + + 工作负载 + + + {{ dimScoreText(row, 'workload') }} + + + + + + + + + 活跃客户 + + + + + + {{ fmtInt(num(row, 'active_friends')) }} + + + + 活跃群(日均) + + {{ fmtAvg(num(row, 'active_rooms_avg')) }} + + + + 消息发/收 + + + {{ fmtInt(num(row, 'msg_sent')) }} / {{ fmtInt(num(row, 'msg_recv')) }} + + + + + 通话量/时长 + + + {{ fmtInt(num(row, 'call_count')) }} / {{ fmtSec(num(row, 'call_seconds')) }} + + + + {{ fmtInt(num(row, 'cust_threads')) }} + + + + + + + + 首响P50 + + {{ formatMsDuration(num(row, 'fr_p50_ms')) }} + + + + + 首响P90 + + + + + + {{ formatMsDuration(num(row, 'fr_p90_ms')) }} + + + + 2h回复率 + + {{ replyRate(row, 'reply_2h_count') }} + + + + 24h回复率 + + {{ replyRate(row, 'reply_24h_count') }} + + + + 被回/串 + + + {{ fmtInt(num(row, 'threads_answered')) }} / {{ fmtInt(num(row, 'cust_threads')) }} + + + + + + + + + 新客 + + {{ fmtInt(num(row, 'new_customer')) }} + + + + 降温 + + {{ fmtInt(num(row, 'cooling_customer')) }} + + + + + 沉睡(期末) + + + + + + {{ fmtInt(num(row, 'dormant_customer')) }} + + + + 回流(期内) + + {{ fmtInt(num(row, 'reactivated_customer')) }} + + + + 活跃占比 + + {{ activeShare(row) }} + + + + + + + + + + + + + + + + + + +