fix: 收敛 Performance summary 展示 (#1648)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<string, string> = { 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<string, string> = { 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 {
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="performance-contract-strip" aria-label="Performance summary 观测契约">
|
||||
<article v-for="item in contractItems" :key="item.key" class="performance-contract-item">
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
<small>{{ item.detail }}</small>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section v-if="!hasDashboard" class="data-panel performance-diagnostic-panel">
|
||||
<EmptyState title="等待图表数据契约" description="summary 已返回,但缺少 dashboard 字段。请先发布包含 chart-ready summary 的 Cloud API,再观察图表。" action-label="刷新" @action="table.reload" />
|
||||
</section>
|
||||
@@ -424,7 +485,11 @@ function toneClass(tone?: string): string {
|
||||
<template #cell-p50="{ row }">{{ formatValue(row as WebPerformanceRow, 'p50') }}</template>
|
||||
<template #cell-p75="{ row }">{{ formatValue(row as WebPerformanceRow, 'p75') }}</template>
|
||||
<template #cell-p95="{ row }"><strong>{{ formatValue(row as WebPerformanceRow, 'p95') }}</strong><small>{{ (row as WebPerformanceRow).problem ? `诊断:${problemLabel((row as WebPerformanceRow).problem)}` : '' }}</small></template>
|
||||
<template #cell-tone="{ row }"><span class="status-pill" :data-status="(row as WebPerformanceRow).tone || 'pending'">{{ statusText(row as WebPerformanceRow) }}</span></template>
|
||||
<template #cell-tone="{ row }">
|
||||
<span class="status-pill" :data-status="(row as WebPerformanceRow).tone || 'pending'">{{ statusText(row as WebPerformanceRow) }}</span>
|
||||
<small class="performance-row-contract">{{ rowContractText(row as WebPerformanceRow) }}</small>
|
||||
<small class="performance-row-contract">{{ rowWindowText(row as WebPerformanceRow) }}</small>
|
||||
</template>
|
||||
</DataTable>
|
||||
</TablePageLayout>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user