From 9493d85259ec177075409ed38f523725d48de4de Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Sat, 20 Jun 2026 02:04:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=94=B6=E6=95=9B=20Performance=20summa?= =?UTF-8?q?ry=20=E5=B1=95=E7=A4=BA=20(#1648)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/hwlab-cloud-web/src/styles/workbench.css | 45 +++++++++++++ .../src/views/PerformanceView.vue | 67 ++++++++++++++++++- 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/web/hwlab-cloud-web/src/styles/workbench.css b/web/hwlab-cloud-web/src/styles/workbench.css index 496643a0..a1e0a987 100644 --- a/web/hwlab-cloud-web/src/styles/workbench.css +++ b/web/hwlab-cloud-web/src/styles/workbench.css @@ -2492,6 +2492,50 @@ border-left-color: #b91c1c; } +.performance-contract-strip { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; + border: 1px solid #dbe3ef; + border-radius: 8px; + background: #ffffff; + padding: 12px; +} + +.performance-contract-item { + display: grid; + min-width: 0; + gap: 4px; + border-left: 3px solid #0e7490; + background: #f8fafc; + padding: 10px; +} + +.performance-contract-item span, +.performance-contract-item small, +.performance-row-contract { + min-width: 0; + overflow: hidden; + color: #64748b; + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.performance-contract-item strong { + min-width: 0; + overflow: hidden; + color: #0f172a; + font-size: 13px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.performance-row-contract { + display: block; + margin-top: 4px; +} + .performance-freshness-panel { display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -2895,6 +2939,7 @@ .system-summary-grid, .system-split-grid, .performance-health-strip, + .performance-contract-strip, .performance-chart-grid, .performance-analytics-grid { grid-template-columns: 1fr; diff --git a/web/hwlab-cloud-web/src/views/PerformanceView.vue b/web/hwlab-cloud-web/src/views/PerformanceView.vue index 6aaf1d7d..9b400073 100644 --- a/web/hwlab-cloud-web/src/views/PerformanceView.vue +++ b/web/hwlab-cloud-web/src/views/PerformanceView.vue @@ -54,12 +54,31 @@ const trends = computed(() => dashboard.value?.trends ?? []); const distributions = computed(() => dashboard.value?.distributions ?? []); const topN = computed(() => dashboard.value?.topN ?? []); const problems = computed(() => payload.value?.problems ?? []); +const sampleWindow = computed(() => dashboard.value?.sampleWindow ?? payload.value?.sampleWindow ?? null); const detailRows = computed(() => { if (problems.value.length) return problems.value; return [...(payload.value?.workbenchRows ?? []), ...(payload.value?.rows ?? [])]; }); const hasDashboard = computed(() => dashboard.value?.schemaVersion === "hwlab-web-performance-dashboard-v1"); const sampleWindowText = computed(() => dashboard.value?.sampleWindow?.label ?? payload.value?.sampleWindow?.label ?? windowLabel(currentWindow.value)); +const sampleWindowRangeText = computed(() => { + const from = sampleWindow.value?.windowFrom ?? summary.value?.windowFrom; + const to = sampleWindow.value?.windowTo ?? summary.value?.windowTo; + if (!from && !to) return "窗口边界未上报"; + return `${formatTime(String(from ?? ""))} - ${formatTime(String(to ?? ""))}`; +}); +const generatedAtText = computed(() => formatTime(String(sampleWindow.value?.generatedAt ?? summary.value?.generatedAt ?? dashboard.value?.generatedAt ?? payload.value?.observedAt ?? ""))); +const coverageText = computed(() => `${formatCount(summary.value?.sampleCount)} 样本 / ${formatCount(summary.value?.routeCount)} 路由`); +const seriesText = computed(() => `${formatCount(summary.value?.durationSeries)} API/浏览器序列 · ${formatCount(summaryNumber("workbenchJourneySeries"))} 工作台序列`); +const qualityText = computed(() => dashboard.value?.freshness?.statusLabel ?? summaryStatusLabel(String(summary.value?.status ?? dashboard.value?.freshness?.status ?? "waiting"))); +const aggregationText = computed(() => aggregationLabel(String(summary.value?.aggregationKind ?? sampleWindow.value?.aggregationKind ?? ""), String(summary.value?.approximation ?? sampleWindow.value?.approximation ?? ""))); +const contractItems = computed(() => [ + { key: "window", label: "观测窗口", value: sampleWindowText.value, detail: sampleWindowRangeText.value }, + { key: "generated", label: "生成时间", value: generatedAtText.value, detail: String(payload.value?.source ?? dashboard.value?.freshness?.source ?? "cloud-api-rum-store") }, + { key: "coverage", label: "样本覆盖", value: coverageText.value, detail: seriesText.value }, + { key: "quality", label: "数据质量", value: qualityText.value, detail: `低样本阈值 ${formatCount(summary.value?.lowSampleThreshold ?? dashboard.value?.freshness?.lowSampleThreshold)} 个样本` }, + { key: "aggregation", label: "统计口径", value: aggregationText.value, detail: `sourceFamily=${String(dashboard.value?.freshness?.sourceFamily ?? "mixed")}` } +]); const freshnessText = computed(() => { const freshness = dashboard.value?.freshness; const observedAt = formatTime(freshness?.observedAt ?? payload.value?.observedAt); @@ -167,6 +186,12 @@ function formatCount(value?: number | string): string { return Number.isFinite(number) ? number.toLocaleString("zh-CN") : "0"; } +function summaryNumber(key: string): number { + const value = summary.value?.[key]; + const number = Number(value); + return Number.isFinite(number) ? number : 0; +} + function formatCardValue(value?: string | number, unit?: string): string { if (typeof value === "number") return `${value.toLocaleString("zh-CN")}${unit && unit !== "状态" ? ` ${unit}` : ""}`; return String(value ?? "-"); @@ -179,6 +204,34 @@ function formatTime(value?: string): string { return new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }).format(date); } +function summaryStatusLabel(value?: string): string { + const labels: Record = { ok: "正常", watch: "需关注", warming: "样本积累中", waiting: "等待数据", blocked: "阻塞", warn: "预警" }; + return labels[String(value ?? "waiting")] ?? String(value ?? "等待数据"); +} + +function aggregationLabel(kind?: string, approximation?: string): string { + if (kind === "exact" && approximation === "exact") return "精确窗口分位"; + if (kind === "histogram" || approximation === "bucketed") return "桶化估算"; + return "统计口径未知"; +} + +function rowContractText(row: WebPerformanceRow): string { + const family = row.sourceFamily ? `来源:${sourceFamilyLabel(row.sourceFamily)}` : "来源:未标注"; + const aggregation = aggregationLabel(String(row.aggregationKind ?? ""), String(row.approximation ?? "")); + return `${family} · ${aggregation}`; +} + +function rowWindowText(row: WebPerformanceRow): string { + const freshness = row.freshness ?? row.sampleState; + const parts = [sampleStateLabel(freshness), row.sampleCount !== undefined ? `${formatCount(row.sampleCount)} 样本` : "", row.windowFrom && row.windowTo ? `${formatTime(row.windowFrom)} - ${formatTime(row.windowTo)}` : ""].filter(Boolean); + return parts.join(" · "); +} + +function sourceFamilyLabel(value?: string): string { + const labels: Record = { web: "浏览器", workbench: "工作台", backend: "后端", mixed: "混合" }; + return labels[String(value ?? "mixed")] ?? String(value); +} + function formatDimensions(row: WebPerformanceRow): string { const parts = [ row.backend ? `后端:${row.backend}` : "", @@ -339,6 +392,14 @@ function toneClass(tone?: string): string { +
+
+ {{ item.label }} + {{ item.value }} + {{ item.detail }} +
+
+
@@ -424,7 +485,11 @@ function toneClass(tone?: string): string { - +