docs(management): 批次C前端计划——驾驶舱+导航(Task C0-C6)
评审4真问题全修:drilldown行补channel字段(C0后端契约补全)/DimSummary 补name+weight/端口3000→8000;advisory全采:服务图双轴/共享格式化提升 format.ts/去硬编码41/client契约字段补全
This commit is contained in:
@@ -0,0 +1,305 @@
|
|||||||
|
# 云客经营管理后台·批次C(前端导航+管理驾驶舱)实施计划
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 交付经营管理后台的第一个可视化页面——管理驾驶舱(`/management/dashboard`)+「经营管理」导航分组;管理者 5 分钟看整体、异常两次点击进事实。
|
||||||
|
|
||||||
|
**Architecture:** 前端纯增量:`api/management.ts`(六端点 typed client,契约=批次B终评冻结面)+ `composables/useManagementFilters.ts`(URL query 为真源)+ `components/management/` 六个组件 + `ManagementDashboardView.vue`。复用 useChart/api client/format utils/seq 守卫惯例;不引新 UI 框架、不引雷达图(维度分布用条形图)。依据 [v2 计划](./2026-09-08-yunke-management-console-implementation-v2.md) §4/§5/§7/§13 与批次B契约清单;员工总览/详情页是批次D。
|
||||||
|
|
||||||
|
**Tech Stack:** Vue3 + TS + Element Plus + ECharts(useChart 按需已有 Line/Bar)+ vitest(若仓库已配)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 执行者必读
|
||||||
|
|
||||||
|
1. **后端就绪**(本地 8000 已跑或按 DEPLOY 起):六端点见 `/tmp` 之外的真实路径 `/api/management/*`;**契约以批次B终评清单为准**(None≠0;`scope.channel` 回显原值;drilldown 白名单六值;异常卡片 12 字段;`config_source=db`)。
|
||||||
|
2. **前端惯例**:路由扁平 + `meta.title` + 登录守卫(public 才放行);API 走 `getJson/requestJson`(client.ts 已带 Bearer+baseURL=/api);图表用 `useChart`(容器稳定,不 v-if);请求序号 seq 守卫防旧响应覆盖(DashboardView 现有模式照抄);格式化放 `utils/format.ts` 不重复实现;el-select 空串 value 显示英文占位用 `'all'` 哨兵。
|
||||||
|
3. **不做**:员工总览/详情页、辅导、快照(D/E 批次);`/analytics/dashboard` 不重定向(并存过渡);二期菜单不渲染不放空页。
|
||||||
|
4. **验收基准**:v2 §5——5 分钟判总体;异常两次点击到事实;全部卡片显示数据截至与对比窗口;`npm run build` 过。
|
||||||
|
5. 仓库有并行会话活动——`git status` 后只 add 自己文件;提交中文 conventional;master 直提。
|
||||||
|
|
||||||
|
## 文件地图
|
||||||
|
|
||||||
|
| 文件 | 职责 | 动作 |
|
||||||
|
|---|---|---|
|
||||||
|
| `yunkes/src/api/management.ts` | 六端点 typed client | 新建 |
|
||||||
|
| `yunkes/src/composables/useManagementFilters.ts` | URL query ↔ 筛选(channel/start/end/compare + 预设窗) | 新建 |
|
||||||
|
| `yunkes/src/components/management/DataAsOfBadge.vue` | 数据截至/版本/试算角标 | 新建 |
|
||||||
|
| `yunkes/src/components/management/ManagementKpiCard.vue` | KPI 卡(主值+辅助+点击跳转) | 新建 |
|
||||||
|
| `yunkes/src/components/management/ManagementFilterBar.vue` | 筛选栏(渠道/时间预设/自定义/口径入口按钮) | 新建 |
|
||||||
|
| `yunkes/src/components/management/AnomalyList.vue` | 异常榜(severity 标签+数字+下钻事件) | 新建 |
|
||||||
|
| `yunkes/src/components/management/MetricDrilldownDrawer.vue` | 下钻抽屉(drilldown 端点行清单+聊天跳转) | 新建 |
|
||||||
|
| `yunkes/src/components/management/MetricDefsDrawer.vue` | 指标口径抽屉(metric-definitions 渲染) | 新建 |
|
||||||
|
| `yunkes/src/views/management/ManagementDashboardView.vue` | 驾驶舱页 | 新建 |
|
||||||
|
| `yunkes/src/router/index.ts` | /management/dashboard 路由 | 修改 |
|
||||||
|
| `yunkes/src/layouts/MainLayout.vue` | 「经营管理」分组 | 修改 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task C0: 后端契约补全——drilldown 行补 channel 字段
|
||||||
|
|
||||||
|
**Files:** Modify `yunke-server/app/management/api_assembly.py` + `yunke-server/tests/test_management_api.py`
|
||||||
|
|
||||||
|
批次B已提交但未部署,前端冻结前补齐(评审发现:fr_fact_rows/lifecycle_customers 的行无 channel 键,前端 channel=all 时 WA 行会跳错聊天路由):
|
||||||
|
1. `fr_fact_rows` 每行加 `channel`(SELECT 已有该列,透传即可);
|
||||||
|
2. `lifecycle_customers` 每行加 `channel`(c.channel 透传);
|
||||||
|
3. 测试断言补两行(drilldown p90 与 cooling/dormant 的响应行含 channel)。
|
||||||
|
- [ ] `cd yunke-server && .venv/Scripts/python.exe -m pytest tests/test_management_api.py -q`(23 passed)+ 全量 -q
|
||||||
|
- [ ] commit `fix(management): drilldown行补channel字段——前端聊天路由判渠道用(契约冻结前补齐)`
|
||||||
|
|
||||||
|
### Task C1: api/management.ts(typed client)
|
||||||
|
|
||||||
|
**Files:** Create `yunkes/src/api/management.ts`
|
||||||
|
|
||||||
|
完整代码(契约=批次B as-built;先对照后端 router 响应键核对再落笔):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 经营管理板块 API(批次C)。契约以批次B终评冻结面为准:
|
||||||
|
// None≠0(无样本分位/率=null);scope.channel 回显原值;drilldown 白名单六值。
|
||||||
|
import { getJson } from './client'
|
||||||
|
import type { Channel } from './types'
|
||||||
|
|
||||||
|
export type MgmtChannel = 'all' | Channel
|
||||||
|
export type CompareMode = 'previous' | 'personal_baseline' | 'team_median'
|
||||||
|
export type DrilldownCode =
|
||||||
|
| 'first_reply_p90' | 'first_reply_p50' | 'unreplied'
|
||||||
|
| 'cooling_customers' | 'dormant_customers' | 'call_followup'
|
||||||
|
|
||||||
|
export interface MgmtScope { channel: MgmtChannel; start: number; end: number; compare?: CompareMode; view?: string; user_id?: string; metric_code?: string }
|
||||||
|
export interface Kpis {
|
||||||
|
active_customers: number; active_employees: number
|
||||||
|
msg_total: number; cust_threads: number; threads_answered: number
|
||||||
|
call_count: number; call_seconds: number; answered_call: number
|
||||||
|
lifecycle: { new: number; cooling: number; dormant: number; reactivated: number }
|
||||||
|
}
|
||||||
|
export interface CommTrendPoint { day: number; msg_total: number; active_customers: number; call_cnt: number }
|
||||||
|
export interface SvcTrendPoint { day: number; fr_p50_ms: number | null; fr_p90_ms: number | null; reply_2h_rate: number | null }
|
||||||
|
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
|
||||||
|
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 } }
|
||||||
|
suggested_action: string
|
||||||
|
}
|
||||||
|
export interface DashboardResp {
|
||||||
|
scope: MgmtScope; data_as_of: number | null; metric_version: string; config_source: 'db' | 'default'
|
||||||
|
kpis: Kpis
|
||||||
|
communication_trend: CommTrendPoint[]
|
||||||
|
service_trend: SvcTrendPoint[]
|
||||||
|
lifecycle: Kpis['lifecycle']
|
||||||
|
dimension_summary: DimSummary
|
||||||
|
anomalies: AnomalyCard[]
|
||||||
|
action_summary: { unreplied_2h: number; churn_real_30d: number; sensitive_pending: number }
|
||||||
|
}
|
||||||
|
export interface EmployeeRow {
|
||||||
|
user_id: string; name: string | null; is_on_job: boolean | null
|
||||||
|
metrics: Record<string, number | null>
|
||||||
|
comparisons: Record<string, { current: number | null; prev: number | null; baseline: number | null; median: number | null }>
|
||||||
|
score: { total_score: number | null; grade: string | null; score_status: string; trial: boolean; dimensions: Record<string, { score: number | null; status: string }> }
|
||||||
|
anomaly_count: number; anomaly_weight: number
|
||||||
|
}
|
||||||
|
export interface EmployeesResp { total: number; page: number; page_size: number; rows: EmployeeRow[]; scope: MgmtScope; data_as_of: number | null; metric_version: string; config_source: 'db' | 'default' }
|
||||||
|
export interface FactDrillRow { day: number; channel: string; owner_account_id: string; peer_id: string; thread_end_ts: number; reply_ts: number; fr_ms: number }
|
||||||
|
export interface LifecycleDrillRow { friend_id: string; channel: string; nickname: string | null; last_day: number; state: string }
|
||||||
|
export interface CallDrillRow { call_id: string; channel: string; call_number: string | null; start_call_time: number; call_duration: number }
|
||||||
|
export interface DrilldownResp { total: number; items: Record<string, unknown>[]; note?: string; truncated?: boolean; summary?: Record<string, number> }
|
||||||
|
export interface MetricDefsResp {
|
||||||
|
definitions: Record<string, { name: string; unit: string; direction: number; caliber?: string; formula?: string }>
|
||||||
|
score_config: { version: string; name: string; trial: boolean; normalization: string; sample_guards: Record<string, number>; grade_bounds: Record<string, number>; dimensions: Record<string, { name: string; weight: number; status?: string }> }
|
||||||
|
caliber_notes: string
|
||||||
|
config_source: 'db' | 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDashboard = (channel: MgmtChannel, start: number, end: number, compare: CompareMode = 'previous') =>
|
||||||
|
getJson<DashboardResp>('/management/dashboard', { channel, start, end, compare })
|
||||||
|
export const getEmployees = (params: Record<string, unknown>) =>
|
||||||
|
getJson<EmployeesResp>('/management/employees', params)
|
||||||
|
export const getEmployeeDrilldown = (userId: string, metricCode: DrilldownCode, start: number, end: number, channel: MgmtChannel, page = 1, pageSize = 20) =>
|
||||||
|
getJson<DrilldownResp>(`/management/employees/${userId}/drilldown`, { metric_code: metricCode, start, end, channel, page, page_size: pageSize })
|
||||||
|
export const getMetricDefs = () => getJson<MetricDefsResp>('/management/metric-definitions')
|
||||||
|
```
|
||||||
|
|
||||||
|
(员工详情/sections 两个端点的 client 函数批次D 再加——本期页面不消费,YAGNI;本期 client 为四函数。)
|
||||||
|
|
||||||
|
- [ ] 步骤:写文件 → `cd yunkes && npx vue-tsc --noEmit`(或 build)类型过 → 后端(8000,dev 5173 的 /api 代理已指向它)冒烟一次 getDashboard 类型与真实响应吻合(console 对比关键字段)→ commit `feat(management): 前端API client——管理端点typed契约(批次B冻结面)`
|
||||||
|
|
||||||
|
### Task C2: useManagementFilters composable
|
||||||
|
|
||||||
|
**Files:** Create `yunkes/src/composables/useManagementFilters.ts`(+ vitest 若仓库已配测试脚本,先 `grep '"test"' yunkes/package.json` 确认)
|
||||||
|
|
||||||
|
完整代码:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 管理页统一筛选(URL query 为真源——刷新/分享保持)。批次C:驾驶舱用;
|
||||||
|
// 批次D 员工页复用(view 等额外参数由页面自行挂 query)。
|
||||||
|
import { computed, watch } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
export type Preset = 'last7' | 'last30' | 'thisMonth' | 'lastMonth' | 'custom'
|
||||||
|
export const CHANNELS = [
|
||||||
|
{ value: 'all', label: '全部渠道' },
|
||||||
|
{ value: 'wechat', label: '微信' },
|
||||||
|
{ value: 'whatsapp', label: 'WhatsApp' },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
function ymd(d: Date): number { return d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate() }
|
||||||
|
function addDays(d: Date, n: number): Date { const x = new Date(d); x.setDate(x.getDate() + n); return x }
|
||||||
|
|
||||||
|
/** 预设 → [start,end] yyyymmdd(上海自然日由本地时区近似——与后端一致的公历日)。 */
|
||||||
|
export function presetRange(p: Preset, today = new Date()): { start: number; end: number } {
|
||||||
|
const t0 = new Date(today.getFullYear(), today.getMonth(), today.getDate())
|
||||||
|
switch (p) {
|
||||||
|
case 'last7': return { start: ymd(addDays(t0, -6)), end: ymd(t0) }
|
||||||
|
case 'last30': return { start: ymd(addDays(t0, -29)), end: ymd(t0) }
|
||||||
|
case 'thisMonth': return { start: ymd(new Date(t0.getFullYear(), t0.getMonth(), 1)), end: ymd(t0) }
|
||||||
|
case 'lastMonth': {
|
||||||
|
const first = new Date(t0.getFullYear(), t0.getMonth() - 1, 1)
|
||||||
|
const last = new Date(t0.getFullYear(), t0.getMonth(), 0)
|
||||||
|
return { start: ymd(first), end: ymd(last) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// custom 由调用方直传
|
||||||
|
return { start: ymd(t0), end: ymd(t0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useManagementFilters() {
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const channel = computed<'all' | 'wechat' | 'whatsapp'>(() => {
|
||||||
|
const v = route.query.channel
|
||||||
|
return v === 'wechat' || v === 'whatsapp' ? v : 'all'
|
||||||
|
})
|
||||||
|
const start = computed(() => {
|
||||||
|
const v = Number(route.query.start)
|
||||||
|
return Number.isFinite(v) && v >= 20000101 ? v : presetRange('last30').start
|
||||||
|
})
|
||||||
|
const end = computed(() => {
|
||||||
|
const v = Number(route.query.end)
|
||||||
|
return Number.isFinite(v) && v >= 20000101 ? v : presetRange('last30').end
|
||||||
|
})
|
||||||
|
const compare = computed(() => {
|
||||||
|
const v = route.query.compare
|
||||||
|
return v === 'personal_baseline' || v === 'team_median' ? v : 'previous'
|
||||||
|
})
|
||||||
|
|
||||||
|
function apply(patch: Record<string, string | number | undefined>) {
|
||||||
|
const q: Record<string, string> = {}
|
||||||
|
for (const [k, v] of Object.entries({ ...route.query, ...patch })) {
|
||||||
|
if (v !== undefined && v !== null && v !== '') q[k] = String(v)
|
||||||
|
}
|
||||||
|
router.replace({ query: q })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预设切换:清掉 start/end 让默认值生效,custom 则带具体日期
|
||||||
|
function applyPreset(p: Preset, custom?: { start: number; end: number }) {
|
||||||
|
if (p === 'custom' && custom) apply({ start: custom.start, end: custom.end })
|
||||||
|
else apply({ start: undefined, end: undefined })
|
||||||
|
}
|
||||||
|
|
||||||
|
return { channel, start, end, compare, apply, applyPreset, presetRange }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] vitest(若已配):presetRange 五预设的日期算术(含月末/跨月)+ query 缺省回退 last30 + apply 写回 query。未配则手测并在报告注明。
|
||||||
|
- [ ] commit `feat(management): useManagementFilters——URL query 真源/预设窗/渠道哨兵`
|
||||||
|
|
||||||
|
### Task C3: 六个基础组件
|
||||||
|
|
||||||
|
**Files:** Create `components/management/` 下六件。规格(完整 SFC 代码进实施,要点钉死):
|
||||||
|
|
||||||
|
1. **DataAsOfBadge**:props `{ dataAsOf: number | null; metricVersion?: string; trial?: boolean; configSource?: string }`;渲染 el-tag 组:数据截至(`data_as_of` ms→`YYYY-MM-DD HH:mm`,null 显示「无数据」灰 tag)+ 指标版本 + 试算(warning 色,trial=true 才显示)。格式化用 utils/format.ts 现有函数,无则组件内小 helper。
|
||||||
|
2. **ManagementKpiCard**:props `{ label: string; value: number | null; unit?: string; hint?: string; to?: string }`;主值大字(null 显示 `--`),hint 小字,有 to 则整卡 clickable `router.push`。不做趋势箭头(对比值批次D 表格里呈现)。
|
||||||
|
3. **ManagementFilterBar**:props modelValue 透传 filters composable 的值 + `@apply`;内容:el-radio-group 渠道(CHANNELS)+ el-select 预设(四预设+自定义)+ 自定义时 el-date-picker type=daterange(value-format YYYYMMDD 字符串转 number)+「指标口径」文字按钮(emit open-defs)+ 插槽放 DataAsOfBadge。改即 apply(防抖 300ms 不需要——change 事件天然低频)。
|
||||||
|
4. **AnomalyList**:props `{ anomalies: AnomalyCard[] }`;el-table 或列表卡片:severity(critical=红 danger tag「严重」/warning=橙 warning tag「关注」)、title、current vs baseline(unit 感知格式化:ms→时长/0-1→%/其他千分位)、sample_size、affected_count、suggested_action(灰小字);行 click → emit `drill(card)`。空态「暂无异常」。
|
||||||
|
5. **MetricDrilldownDrawer**:props `{ userId: string; card: AnomalyCard | null; start: number; end: number; channel: MgmtChannel }`(v-model:visible);打开时按 card.drilldown.query.metric_code 调 getEmployeeDrilldown:
|
||||||
|
- `first_reply_p90/p50` → 表格:客户(peer_id)/串末条时间/回复时间/首响时长(fr_ms 降序已由后端);行点击跳聊天:`/staff-chat/{owner_account_id}/single/{peer_id}`(channel=whatsapp 时 `/chat/whatsapp/{owner}/{peer}`)。
|
||||||
|
- `unreplied` → 表格 peer/等待时长/预览;note/truncated 提示。
|
||||||
|
- `cooling/dormant_customers` → 表格 客户/最后互动日/状态;行点击跳该客户聊天(同上映射)。
|
||||||
|
- `call_followup` → 表格 通话(时间/号码/时长)+ summary 卡。
|
||||||
|
分页 el-pagination(后端分页);员工姓名/标题显示 card.employee_user_id 对应名字由父传(dashboard anomalies 无 name——显示 user_id 短版即可,批次D 带名字)。
|
||||||
|
6. **MetricDefsDrawer**:v-model:visible;打开调 getMetricDefs;el-collapse 按 维度/单指标 两组渲染:每指标 name+unit+direction(越大越好/越小越好)+formula+caliber;顶部 score_config 摘要(版本/试算标记/样本门槛/各维度权重与 pending 状态)+ caliber_notes 段落。
|
||||||
|
|
||||||
|
- [ ] 步骤:六组件逐个写 + vue-tsc 过 → 冒烟渲染(C5 页面里真用)→ commit `feat(management): 驾驶舱六组件——KPI卡/筛选栏/异常榜/下钻抽屉/口径抽屉/数据截至角标`
|
||||||
|
|
||||||
|
### Task C4: 路由 + 导航
|
||||||
|
|
||||||
|
- router/index.ts 在 analytics 四页之后加:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
// 经营管理板块(批次C):驾驶舱;员工分析/考核快照批次D/E再加
|
||||||
|
path: '/management/dashboard',
|
||||||
|
name: 'management-dashboard',
|
||||||
|
component: () => import('@/views/management/ManagementDashboardView.vue'),
|
||||||
|
meta: { title: '管理驾驶舱' },
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
- MainLayout「数据分析」el-sub-menu 之前加「经营管理」分组(单项:管理驾驶舱 → goPlain('/management/dashboard'))——分组在最上(管理优先级高于查看)。
|
||||||
|
- 验证:登录态点菜单进页面、刷新保持、activeMenu 高亮正确。
|
||||||
|
|
||||||
|
- [ ] commit `feat(management): 经营管理导航分组+驾驶舱路由`
|
||||||
|
|
||||||
|
### Task C5: ManagementDashboardView
|
||||||
|
|
||||||
|
**Files:** Create `yunkes/src/views/management/ManagementDashboardView.vue`
|
||||||
|
|
||||||
|
结构(完整 SFC 进实施,要点钉死):
|
||||||
|
|
||||||
|
```text
|
||||||
|
<ManagementFilterBar>(含 DataAsOfBadge + 口径按钮)
|
||||||
|
六个 KPI 卡(el-row/gutter,1366px 两行三卡):
|
||||||
|
活跃客户(kpis.active_customers,hint=串/被回)→无跳转(批次D员工页)
|
||||||
|
活跃员工(kpis.active_employees,hint=期内有数据天数>0 口径)
|
||||||
|
消息量(msg_total,hint=1:1口径·与数据看板口径不同)
|
||||||
|
待回复(action_summary.unreplied_2h,红点>0)→/analytics/unreplied
|
||||||
|
即将流失(action_summary.churn_real_30d)→/analytics/churn
|
||||||
|
风险待处理(action_summary.sensitive_pending)→/analytics/sensitive
|
||||||
|
双图表(el-row 两列,useChart×2):
|
||||||
|
客户沟通趋势:Line msg_total + Bar call_cnt 双 y 轴(x=day)
|
||||||
|
服务质量趋势:双 y 轴——左轴 ms(Line fr_p50_ms/fr_p90_ms,tooltip 时长格式化)、
|
||||||
|
右轴 0-1(Line reply_2h_rate,%)——不注明双轴则率线贴底
|
||||||
|
——图内空数据/None 点处理:reply_2h_rate null 断点(connectNulls=false)
|
||||||
|
双条形图(el-row 两列):
|
||||||
|
生命周期分布:Bar new/cooling/dormant/reactivated 四类
|
||||||
|
员工维度分布:Bar dimension_summary 三维 avg_score(0-100)+pending 数 tooltip
|
||||||
|
异常榜(AnomalyList @drill → MetricDrilldownDrawer)
|
||||||
|
seq 守卫:filters 变化 → seq++ → 请求 → 回来校验 seq 才渲染(照 DashboardView 模式)
|
||||||
|
loading:el-skeleton 首屏;错误:el-alert + 重试按钮(保留上次数据)
|
||||||
|
```
|
||||||
|
|
||||||
|
要点:
|
||||||
|
- `watch([channel, start, end, compare], load)`;`onMounted(load)`;
|
||||||
|
- day 轴 `20260810`→`08-10` 短格式;
|
||||||
|
- **共享格式化**:把 DashboardView 里的 `fmtFr`(ms→时长)/`fmtPct`(0-1→%) 提升为 `utils/format.ts` 的 `formatMsDuration`/`formatRatio`(AnomalyList/抽屉/图表 tooltip 三处消费,不复制第三份);
|
||||||
|
- 异常榜点卡片 → drawer(metric_code 白名单外 type=employee 的卡片——activity_drop/overload 无 drilldown 端点,drawer 显示「员工分析批次D上线」占位说明——**两类 drilldown.type=employee 的卡片点击行为:显示 el-empty 占位**);
|
||||||
|
- 试算角标常显(config_source 透传)。
|
||||||
|
|
||||||
|
- [ ] 步骤:写页面 → `npm run build` 过 → 本地前后端真机走查(起 8000 后端 + `npm run dev` 5173,Playwright 或手动):六卡有数、双图渲染、异常卡点开抽屉、抽屉行跳聊天、口径抽屉渲染、刷新 URL 保持、1366px 布局不塌 → commit `feat(management): 管理驾驶舱页——KPI/双趋势/生命周期/维度分布/异常两次点击到事实`
|
||||||
|
|
||||||
|
### Task C6: 收尾验收
|
||||||
|
|
||||||
|
- [ ] `cd yunkes && npm run build`(0 error);
|
||||||
|
- [ ] `npm run test --silent`(若配了 vitest)或记录未配;
|
||||||
|
- [ ] 验收清单核对(v2 §5.3/§19.1 驾驶舱子集):5 分钟判总体 ✓/异常两次点击进事实 ✓/口径入口 ✓/数据截至+对比窗口显示 ✓/渠道筛选 ✓/URL 保持 ✓/试算标记 ✓/空态与错误态 ✓;
|
||||||
|
- [ ] 全量后端测试仍绿(前端不动后端,确认无意外);
|
||||||
|
- [ ] commit(若剩零散修正)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 批次C验收清单
|
||||||
|
|
||||||
|
- [ ] 「经营管理」分组出现且仅含管理驾驶舱;
|
||||||
|
- [ ] 六 KPI 卡数字与后端响应一致(抽查待回复/风险与 analytics 页对得上);
|
||||||
|
- [ ] 双趋势图渲染 30 点、None 断点正确;
|
||||||
|
- [ ] 异常卡 critical 红标在前;点击 metric 类卡片→抽屉→行→聊天定位(两次点击到事实);
|
||||||
|
- [ ] employee 类异常卡占位不报错;
|
||||||
|
- [ ] 口径抽屉含试算标记与三方口径差异说明;
|
||||||
|
- [ ] URL query 刷新/分享保持筛选;
|
||||||
|
- [ ] `npm run build` 过;后端 722 测试不受影响。
|
||||||
|
|
||||||
|
## 批次D-F
|
||||||
|
|
||||||
|
D:员工总览+详情页(消费 employees/detail/sections 端点+指标解释抽屉);E:辅导闭环+考核快照(写端点);F:调度接入+dashboard 性能修复(unreplied 索引)+部署上线。
|
||||||
Reference in New Issue
Block a user