From 4ac73fff49408cc08dd693085bb3dbb3e96cf86a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Fri, 4 Sep 2026 22:02:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(analytics):=20=E7=9C=8B=E6=9D=BF=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E6=98=8E=E7=BB=86=E8=AF=8A=E6=96=AD=E4=BC=98=E5=85=88?= =?UTF-8?q?=E6=8E=92=E5=BA=8F+=E8=A7=92=E6=A0=87=E5=A4=8D=E7=94=A8formatTi?= =?UTF-8?q?meShort+health=E8=A7=A3=E8=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- yunkes/src/views/analytics/DashboardView.vue | 50 +++++++++++++------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/yunkes/src/views/analytics/DashboardView.vue b/yunkes/src/views/analytics/DashboardView.vue index 1d50bd8..36d2010 100644 --- a/yunkes/src/views/analytics/DashboardView.vue +++ b/yunkes/src/views/analytics/DashboardView.vue @@ -57,9 +57,10 @@ // 数据来自 /analytics/overview + /analytics/health(数据截至角标)。 import { computed, onMounted, ref, watch } from 'vue' import { - getAnalyticsHealth, getOverview, type Channel, type OverviewResp, type Scope, + getAnalyticsHealth, getOverview, type AnomalyStaff, type Channel, type OverviewResp, type Scope, } from '@/api/analytics' import { useChart } from '@/composables/useChart' +import { formatTimeShort } from '@/utils/format' interface CardItem { label: string @@ -101,10 +102,9 @@ async function load() { loading.value = true error.value = '' try { - const [ov, h] = await Promise.all([getOverview(channel.value, scope.value), getAnalyticsHealth()]) + const ov = await getOverview(channel.value, scope.value) if (my !== seq) return data.value = ov - asOf.value = h.data_as_of } catch (e) { if (my === seq) error.value = e instanceof Error ? e.message : String(e) } finally { @@ -113,12 +113,15 @@ async function load() { } } -const asOfText = computed(() => { - if (!asOf.value) return '' - const d = new Date(asOf.value) - const pad = (n: number) => String(n).padStart(2, '0') - return `${d.getMonth() + 1}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}` -}) +const asOfText = computed(() => formatTimeShort(asOf.value)) + +// 异常员工偏离分:相对前 7 日均值的变化幅度;无均值但昨日有量/新增 → 最高优先 +const anomalyDeviation = (s: AnomalyStaff) => + s.prev7_mean > 0 + ? Math.abs(s.yesterday - s.prev7_mean) / s.prev7_mean + : s.yesterday > 0 || s.new + ? Infinity + : 0 // 异常警示展示模型:判空/格式化收在 computed,模板只读定型字段(slot 闭包里 // 无法沿用 v-if 的空值窄化,预拍平最稳) @@ -131,12 +134,18 @@ const anomalyView = computed(() => { yesterday: a.yesterday_total, mean: a.prev7_mean, staffCount: a.staff.length, - topStaff: a.staff.slice(0, 5).map((s) => ({ - user_id: s.user_id, - yesterday: s.yesterday, - prev7_mean: s.prev7_mean, - isNew: !!s.new, - })), + // 截前 5 条前按「诊断价值」重排,不能沿用后端 yesterday 降序——归零 + // (掉同步/离职,spec §3.2 核心用例)恰在降序尾部,掉线事故(>5 人被 + // 标记)时最先被裁掉。归零/新增置顶,其余按偏离幅度降序。 + topStaff: [...a.staff] + .sort((x, y) => anomalyDeviation(y) - anomalyDeviation(x)) + .slice(0, 5) + .map((s) => ({ + user_id: s.user_id, + yesterday: s.yesterday, + prev7_mean: s.prev7_mean, + isNew: !!s.new, + })), } }) @@ -189,7 +198,16 @@ function draw() { // post:等 DOM 更新后再画,保证首帧 chartEl 已挂载(趋势图恒 30 天,随 data 重画) watch([data, trendMetric], draw, { flush: 'post' }) -onMounted(load) +onMounted(() => { + load() + // health(数据截至)与筛选无关,挂载拉一次即可;失败只影响角标(静默隐藏), + // 不进页面 error alert + getAnalyticsHealth() + .then((h) => { + asOf.value = h.data_as_of + }) + .catch(() => {}) +})