<!-- SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0. -->
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { systemAPI } from "@/api";
import DataTable from "@/components/common/DataTable.vue";
import type { DataTableColumn } from "@/components/common/DataTable.vue";
import EmptyState from "@/components/common/EmptyState.vue";
import LoadingState from "@/components/common/LoadingState.vue";
import PageHeader from "@/components/common/PageHeader.vue";
import TablePageLayout from "@/components/layout/TablePageLayout.vue";
import { useTableLoader } from "@/composables/useTableLoader";
import type {
  WebPerformanceCollectionHealthDiagnostic,
  WebPerformanceDashboardDistribution,
  WebPerformanceDashboardPoint,
  WebPerformanceDashboardTopN,
  WebPerformanceDashboardTrend,
  WebPerformanceRow,
  WebPerformanceSummaryResponse,
  WebPerformanceWindow
} from "@/types";

const performanceWindowOptions: Array<{ value: WebPerformanceWindow; label: string }> = [
  { value: "5m", label: "5 分钟" },
  { value: "15m", label: "15 分钟" },
  { value: "1h", label: "1 小时" },
  { value: "6h", label: "6 小时" },
  { value: "24h", label: "24 小时" },
  { value: "all", label: "全部" }
];

const detailColumns: DataTableColumn[] = [
  { key: "metric", label: "指标" },
  { key: "route", label: "路由与维度" },
  { key: "count", label: "样本", align: "right" },
  { key: "p50", label: "P50", align: "right" },
  { key: "p75", label: "P75", align: "right" },
  { key: "p95", label: "P95", align: "right" },
  { key: "tone", label: "健康状态" }
];

const payload = ref<WebPerformanceSummaryResponse | null>(null);
const currentWindow = ref<WebPerformanceWindow>("15m");
const table = useTableLoader<WebPerformanceRow>(async () => {
  const response = await systemAPI.performanceSummary(currentWindow.value);
  if (!response.ok) throw response;
  payload.value = response.data;
  return response.data?.rows ?? [];
});

const summary = computed(() => payload.value?.summary ?? {});
const dashboard = computed(() => payload.value?.dashboard ?? null);
const cards = computed(() => dashboard.value?.cards ?? []);
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 collectionHealth = computed(() => dashboard.value?.collectionHealth ?? payload.value?.collectionHealth ?? null);
const collectionDiagnostics = computed(() => (collectionHealth.value?.diagnostics ?? []).slice(0, 4));
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);
  const source = freshness?.source ?? payload.value?.source ?? "-";
  const namespace = freshness?.namespace ?? payload.value?.namespace ?? "-";
  const target = freshness?.gitopsTarget ?? payload.value?.gitopsTarget ?? "-";
  return `${sampleWindowText.value} · ${observedAt} · ${source} · ${namespace} / ${target}`;
});
const collectionHealthItems = computed(() => {
  const health = collectionHealth.value;
  if (!health) return [
    { key: "collection-missing", label: "采集健康", value: "等待字段", detail: "summary 暂未返回 collectionHealth" }
  ];
  return [
    {
      key: "last-sample",
      label: "最近样本",
      value: formatTime(String(health.lastSampleAt ?? health.lastRetainedSampleAt ?? "")),
      detail: `${String(health.reasonLabel ?? "采集状态未知")} · ${formatSecondsSince(health.secondsSinceLastSample)}`
    },
    {
      key: "window-samples",
      label: "当前窗口样本",
      value: `${formatCount(health.sampleCount)} 样本`,
      detail: `保留 ${formatCount(health.retainedSampleCount)} 样本 · 覆盖 ${formatWindowCoverage(health.windowCoverageSeconds)}`
    },
    {
      key: "source-family",
      label: "来源组成",
      value: sourceFamilySummary(health.sourceFamilyCounts),
      detail: `${formatCount(health.sampleSeries)} 个序列 · 低样本阈值 ${formatCount(health.lowSampleThreshold)}`
    },
    {
      key: "unknown-attribution",
      label: "归因完整性",
      value: unknownAttributionLabel(health.unknownAttribution),
      detail: unknownAttributionDetail(health.unknownAttribution)
    }
  ];
});

onMounted(() => void table.reload());

function rowKey(row: WebPerformanceRow): string {
  return `${row.kind ?? "kind"}:${row.metric ?? "metric"}:${row.route ?? "route"}:${row.method ?? "method"}:${row.statusClass ?? "status"}:${row.backend ?? "backend"}:${row.transport ?? "transport"}:${row.eventType ?? "event"}:${row.phase ?? "phase"}`;
}

function windowLabel(value: WebPerformanceWindow): string {
  return performanceWindowOptions.find((option) => option.value === value)?.label ?? "15 分钟";
}

function handleWindowChange(value: WebPerformanceWindow) {
  if (currentWindow.value === value) return;
  currentWindow.value = value;
  void table.reload();
}

function displayText(value?: string | null): string {
  return String(value ?? "")
    .replace(/:sessionId/gu, ":会话")
    .replace(/:traceId/gu, ":轨迹")
    .replace(/:runId/gu, ":运行")
    .replace(/:threadId/gu, ":线程")
    .replace(/:turnId/gu, ":轮次")
    .replace(/\bsessionId\b/gu, "会话")
    .replace(/\btraceId\b/gu, "轨迹")
    .replace(/\brunId\b/gu, "运行")
    .replace(/\bthreadId\b/gu, "线程")
    .replace(/\bturnId\b/gu, "轮次")
    .replace(/\bstdout\b/gu, "标准输出")
    .replace(/\bstderr\b/gu, "错误输出")
    .replace(/\bSecret\b/gu, "密钥");
}

function displayRoute(value?: string | null): string {
  const text = displayText(value).trim();
  return text || "当前窗口";
}

function metricLabel(row: WebPerformanceRow): string {
  const metric = String(row.phase || row.metric || row.kind || "unknown").replace(/-/gu, "_");
  const labels: Record<string, string> = {
    submit_to_first_visible: "提交到首个可见结果",
    submit_to_failure: "提交失败可见",
    backend_event_to_visible: "后端事件到可见",
    session_switch_first_visible: "切换会话首个可见",
    session_switch_full_load: "切换会话完整加载",
    workbench_open_first_visible: "打开工作台首个可见",
    workbench_open_full_load: "打开工作台完整加载",
    created_to_append: "创建到写入 Trace",
    append_to_sse: "Trace 写入到实时流",
    sse_to_receive: "实时流到浏览器",
    receive_to_project: "接收到状态投影",
    project_to_paint: "投影到画面可见",
    user_submit_to_api_accepted: "提交到 API 接收",
    api_accepted_to_backend_event: "API 接收到后端事件",
    api_request: "同源 API 请求",
    long_task: "主线程长任务",
    lcp: "最大内容绘制",
    inp: "交互响应",
    fid: "首次输入延迟",
    cls: "布局偏移",
    navigation_ttfb: "首字节时间",
    navigation_dom_content_loaded: "DOM 内容加载",
    navigation_load: "页面加载完成",
    trace_event_projected: "轨迹事件投影",
    backend_event_visible: "后端事件到可见",
    cache_hit_ratio: "缓存命中率",
    db_query_avoided: "DB 查询避免率",
    cache_unavailable: "缓存不可用率",
    sessions_api_p95: "会话 API P95",
    longtask: "主线程长任务"
  };
  return labels[metric] ?? metric.replace(/[_-]/gu, " ");
}

function kindLabel(kind?: string): string {
  const labels: Record<string, string> = {
    workbench_journey: "工作台体感",
    workbench_event_phase: "事件阶段",
    workbench_backend_event_visible: "后端可见",
    workbench_cache: "缓存观测",
    web_vital: "浏览器体感",
    navigation: "页面加载",
    api: "同源 API",
    long_task: "长任务"
  };
  return labels[String(kind ?? "")] ?? "性能样本";
}

function formatValue(row: WebPerformanceRow | WebPerformanceDashboardPoint, key: "value" | "p50" | "p75" | "p95" = "value", unit?: string): string {
  const value = Number(row[key]);
  if (!Number.isFinite(value)) return "-";
  const resolvedUnit = unit ?? ("unit" in row && typeof row.unit === "string" ? row.unit : undefined);
  if (resolvedUnit === "score") return value.toFixed(3);
  if (value >= 1) return `${value.toFixed(value >= 10 ? 0 : 1)} 秒`;
  return `${Math.round(value * 1000)} 毫秒`;
}

function formatCount(value?: number | string): string {
  const number = Number(value);
  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 ?? "-");
}

function formatTime(value?: string): string {
  if (!value) return "未采集";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return value;
  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 backendEvidenceText(row: WebPerformanceRow): string {
  const evidence = row.backendEvidence;
  if (!evidence) return "";
  const status = evidence.statusLabel || backendEvidenceStatusLabel(evidence.status);
  if (evidence.status === "matched") {
    const parts = [
      "后端证据",
      status,
      evidence.diagnosticLabel,
      `${formatCount(evidence.count)} 样本`,
      Number.isFinite(Number(evidence.p95)) ? `P95 ${formatValue({ p95: Number(evidence.p95), unit: "seconds" }, "p95")}` : "",
      Number.isFinite(Number(evidence.responseBytesP95)) ? `响应 P95 ${formatBytes(Number(evidence.responseBytesP95))}` : ""
    ].filter(Boolean);
    return parts.join(" · ");
  }
  return ["后端证据", status, evidence.diagnosticLabel || evidence.detail].filter(Boolean).join(" · ");
}

function backendEvidenceStatusLabel(value?: string): string {
  const labels: Record<string, string> = { matched: "已匹配后端", missing: "后端无匹配样本", unavailable: "后端证据未接入" };
  return labels[String(value ?? "unavailable")] ?? String(value ?? "后端证据未知");
}

function formatBytes(value?: number): string {
  const number = Number(value);
  if (!Number.isFinite(number)) return "-";
  if (number < 1024) return `${Math.round(number)} B`;
  if (number < 1024 * 1024) return `${(number / 1024).toFixed(number >= 10 * 1024 ? 0 : 1)} KiB`;
  return `${(number / 1024 / 1024).toFixed(1)} MiB`;
}

function sourceFamilyLabel(value?: string): string {
  const labels: Record<string, string> = { web: "浏览器", workbench: "工作台", backend: "后端", mixed: "混合", rum_api_timing: "同源 API", rum_web_vitals: "浏览器指标", rum_navigation: "页面加载", rum_long_task: "长任务", workbench_journey: "工作台体感", workbench_event_phase: "事件阶段", workbench_backend_event_visible: "后端可见", unknown: "未知" };
  return labels[String(value ?? "mixed")] ?? String(value);
}

function formatSecondsSince(value?: number | null): string {
  const number = Number(value);
  if (!Number.isFinite(number)) return "没有最近样本";
  if (number < 60) return `${Math.max(0, Math.round(number))} 秒前`;
  if (number < 3600) return `${Math.round(number / 60)} 分钟前`;
  return `${Math.round(number / 3600)} 小时前`;
}

function formatWindowCoverage(value?: number | null): string {
  const number = Number(value);
  if (!Number.isFinite(number)) return "无窗口覆盖";
  if (number < 60) return `${Math.max(0, Math.round(number))} 秒`;
  if (number < 3600) return `${Math.round(number / 60)} 分钟`;
  return `${Math.round(number / 3600)} 小时`;
}

function sourceFamilySummary(value?: Array<{ sourceFamily?: string; sampleCount?: number }>): string {
  const items = Array.isArray(value) ? value : [];
  if (!items.length) return "暂无来源";
  return items.slice(0, 2).map((item) => `${sourceFamilyLabel(item.sourceFamily)} ${formatCount(item.sampleCount)}`).join(" / ");
}

function unknownAttributionLabel(value?: { rowsWithUnknown?: number }): string {
  const rows = Number(value?.rowsWithUnknown ?? 0);
  return rows > 0 ? `${formatCount(rows)} 条不完整` : "归因完整";
}

function unknownAttributionDetail(value?: { samplesWithUnknown?: number; backendUnknownRows?: number; transportUnknownRows?: number }): string {
  const samples = Number(value?.samplesWithUnknown ?? 0);
  const backend = Number(value?.backendUnknownRows ?? 0);
  const transport = Number(value?.transportUnknownRows ?? 0);
  if (samples <= 0 && backend <= 0 && transport <= 0) return "backend / transport 均已归因";
  return `${formatCount(samples)} 样本 · backend unknown ${formatCount(backend)} 条 · transport unknown ${formatCount(transport)} 条`;
}

function formatDimensions(row: WebPerformanceRow): string {
  const parts = [
    row.backend ? `后端：${row.backend}` : "",
    row.transport ? `传输：${transportLabel(row.transport)}` : "",
    row.eventType ? `事件：${eventTypeLabel(row.eventType)}` : "",
    row.targetState ? `目标：${targetStateLabel(row.targetState)}` : "",
    row.cache ? `缓存：${cacheLabel(row.cache)}` : "",
    row.source ? `来源：${sourceLabel(row.source)}` : "",
    row.entry ? `入口：${entryLabel(row.entry)}` : "",
    row.outcome ? `结果：${outcomeLabel(row.outcome)}` : ""
  ].filter(Boolean);
  return parts.length ? parts.join(" / ") : "当前窗口";
}

function problemLabel(problem?: string): string {
  const text = String(problem ?? "");
  const labels: Record<string, string> = {
    ok: "正常",
    "low-sample": "低样本",
    timeout: "超时",
    network: "网络失败",
    "server-error": "服务端错误",
    "client-error": "客户端错误",
    "layout-shift": "布局偏移",
    "main-thread busy": "主线程繁忙"
  };
  if (labels[text]) return labels[text];
  if (text.startsWith("slow-")) return `慢路径：${metricLabel({ metric: text.slice(5) } as WebPerformanceRow)}`;
  return text.replace(/[_-]/gu, " ");
}

function statusText(row: WebPerformanceRow): string {
  return `${toneLabel(row.tone)} · ${sampleStateLabel(row.sampleState)}`;
}

function toneLabel(tone?: string): string {
  if (tone === "ok") return "正常";
  if (tone === "warn") return "预警";
  if (tone === "blocked") return "阻塞";
  return "等待数据";
}

function sampleStateLabel(state?: string): string {
  if (state === "ok") return "正常样本";
  if (state === "low-sample") return "低样本";
  return "暂无数据";
}

function outcomeLabel(value?: string): string {
  const labels: Record<string, string> = { ok: "正常", timeout: "超时", error: "错误", dropped: "丢弃", stale: "滞后", partial: "部分", empty: "空结果", network: "网络", denied: "拒绝", unknown: "未知" };
  return labels[String(value ?? "unknown")] ?? String(value);
}

function transportLabel(value?: string): string {
  const labels: Record<string, string> = { sse: "实时流", rest_gap: "REST 补洞", poll: "轮询", none: "无", unknown: "未知" };
  return labels[String(value ?? "unknown")] ?? String(value);
}

function eventTypeLabel(value?: string): string {
  if (String(value ?? "").startsWith("agentrun.")) return "后端输出";
  const labels: Record<string, string> = { assistant: "助手消息", tool_call: "工具调用", backend: "后端事件", terminal: "终态结果", error: "错误", status: "状态", request: "请求", result: "结果", unknown: "未知" };
  return labels[String(value ?? "unknown")] ?? String(value);
}

function targetStateLabel(value?: string): string {
  const labels: Record<string, string> = { running: "运行中", terminal: "已终态", completed: "已完成", empty: "空会话", unknown: "未知" };
  return labels[String(value ?? "unknown")] ?? String(value);
}

function cacheLabel(value?: string): string {
  const labels: Record<string, string> = { warm: "热缓存", cold: "冷加载", hit: "命中", miss: "未命中", sessions_summary: "会话摘要", terminal_turn_snapshot: "终态轮次快照", trace_terminal_events: "终态轨迹事件", unknown: "未知" };
  return labels[String(value ?? "unknown")] ?? String(value);
}

function sourceLabel(value?: string): string {
  const labels: Record<string, string> = { rail: "会话栏", deeplink: "深链", history: "历史", direct: "直达", hydrate: "恢复", unknown: "未知" };
  return labels[String(value ?? "unknown")] ?? String(value);
}

function entryLabel(value?: string): string {
  const labels: Record<string, string> = { new: "新会话", existing: "已有会话", steer: "续写", retry: "重试", unknown: "未知" };
  return labels[String(value ?? "unknown")] ?? String(value);
}

function trendPoints(trend: WebPerformanceDashboardTrend): WebPerformanceDashboardPoint[] {
  return Array.isArray(trend.points) ? trend.points : [];
}

function linePoints(trend: WebPerformanceDashboardTrend): string {
  const points = trendPoints(trend);
  if (!points.length) return "";
  return points.map((point, index) => `${chartX(index, points.length)},${chartY(Number(point.value), points)}`).join(" ");
}

function chartX(index: number, count: number): number {
  if (count <= 1) return 150;
  return Math.round(22 + (index / (count - 1)) * 256);
}

function chartY(value: number, points: WebPerformanceDashboardPoint[]): number {
  const values = points.map((point) => Number(point.value)).filter(Number.isFinite);
  const max = Math.max(...values, 0.001);
  const normalized = Number.isFinite(value) ? value / max : 0;
  return Math.round(86 - normalized * 58);
}

function barPercent(value?: number, max = 1): number {
  const number = Number(value);
  if (!Number.isFinite(number) || number <= 0) return 4;
  return Math.max(6, Math.min(100, (number / Math.max(1, max)) * 100));
}

function distributionMax(distribution: WebPerformanceDashboardDistribution): number {
  return Math.max(1, ...(distribution.buckets ?? []).map((bucket) => Number(bucket.count) || 0));
}

function topMax(group: WebPerformanceDashboardTopN): number {
  return Math.max(0.001, ...(group.rows ?? []).map((row) => Number(row.value) || 0));
}

function toneClass(tone?: string): string {
  if (tone === "blocked") return "is-blocked";
  if (tone === "warn") return "is-warn";
  if (tone === "ok") return "is-ok";
  return "is-source";
}

function diagnosticToneClass(diagnostic: WebPerformanceCollectionHealthDiagnostic): string {
  return toneClass(String(diagnostic.tone ?? "source"));
}
</script>

<template>
  <section class="route-stack system-page performance-page">
    <PageHeader eyebrow="运维监控" title="性能监控" description="以同一性能摘要展示工作台体感、后端事件可见延迟、API 耗时、浏览器指标和采集健康。" />
    <section class="performance-window-toolbar" aria-label="性能时间窗口">
      <div class="performance-window-tabs" role="tablist" aria-label="时间窗口">
        <button
          v-for="option in performanceWindowOptions"
          :key="option.value"
          type="button"
          role="tab"
          :aria-selected="currentWindow === option.value"
          :data-selected="currentWindow === option.value"
          @click="handleWindowChange(option.value)"
        >
          {{ option.label }}
        </button>
      </div>
      <span>{{ sampleWindowText }}</span>
    </section>
    <LoadingState v-if="table.loading.value && !table.rows.value.length" />
    <section v-else-if="table.error.value" class="data-panel">
      <EmptyState title="性能监控加载失败" :description="table.error.value" :api-error="table.apiError.value" :diagnostic="table.diagnostic.value" action-label="重试" @action="table.reload" />
    </section>
    <template v-else>
      <section class="performance-health-strip" aria-label="性能概览">
        <article v-for="card in cards" :key="card.id" class="metric-card performance-health-card" :class="toneClass(String(card.tone ?? 'source'))">
          <span>{{ card.label }}</span>
          <strong>{{ formatCardValue(card.value, card.unit) }}</strong>
          <small>{{ card.detail || freshnessText }}</small>
        </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>

      <template v-else>
        <section class="performance-freshness-panel" aria-label="采集新鲜度">
          <div>
            <strong>{{ dashboard?.freshness?.statusLabel || '等待数据' }}</strong>
            <span>{{ freshnessText }}</span>
          </div>
          <button class="table-action" type="button" :disabled="table.loading.value" @click="table.reload">刷新</button>
        </section>

        <section class="performance-collection-strip" aria-label="采集健康明细">
          <article v-for="item in collectionHealthItems" :key="item.key" class="performance-collection-item">
            <span>{{ item.label }}</span>
            <strong>{{ item.value }}</strong>
            <small>{{ item.detail }}</small>
          </article>
        </section>

        <ul v-if="collectionDiagnostics.length" class="performance-collection-diagnostics" aria-label="采集诊断">
          <li v-for="diagnostic in collectionDiagnostics" :key="diagnostic.code" :class="diagnosticToneClass(diagnostic)">
            <strong>{{ diagnostic.label }}</strong>
            <small>{{ diagnostic.detail }}</small>
          </li>
        </ul>

        <section class="performance-chart-grid" aria-label="性能趋势图">
          <article v-for="trend in trends" :key="trend.id" class="performance-chart-card">
            <header>
              <div>
                <strong>{{ trend.title }}</strong>
                <span>{{ trend.subtitle }}</span>
              </div>
              <em>{{ trendPoints(trend).length }} 条序列</em>
            </header>
            <svg class="performance-line-chart" viewBox="0 0 300 104" role="img" :aria-label="`${trend.title || '性能'}趋势图`">
              <path d="M22 28 H278 M22 57 H278 M22 86 H278" class="chart-grid-line" />
              <polyline v-if="linePoints(trend)" :points="linePoints(trend)" class="chart-line" />
              <circle v-for="(point, index) in trendPoints(trend)" :key="`${trend.id}-${point.label}-${index}`" :cx="chartX(index, trendPoints(trend).length)" :cy="chartY(Number(point.value), trendPoints(trend))" r="4" :class="['chart-point', toneClass(point.tone)]">
                <title>{{ point.label }}：{{ formatValue(point, 'value', trend.unit) }}，{{ formatCount(point.count) }} 个样本</title>
              </circle>
            </svg>
            <ol class="performance-chart-legend">
              <li v-for="point in trendPoints(trend).slice(0, 4)" :key="`${trend.id}-${point.label}`">
                <span>{{ point.label }}</span>
                <strong>{{ formatValue(point, 'value', trend.unit) }}</strong>
              </li>
            </ol>
          </article>
        </section>

        <section class="performance-analytics-grid" aria-label="性能分布与排行">
          <article v-for="distribution in distributions" :key="distribution.id" class="performance-chart-card performance-bar-card">
            <header>
              <div>
                <strong>{{ distribution.title }}</strong>
                <span>{{ distribution.subtitle }}</span>
              </div>
            </header>
            <div class="performance-bar-list">
              <div v-for="bucket in distribution.buckets" :key="`${distribution.id}-${bucket.label}`" class="performance-bar-row">
                <span>{{ bucket.label }}</span>
                <div><i :class="toneClass(bucket.tone)" :style="{ width: `${barPercent(Number(bucket.count), distributionMax(distribution))}%` }" /></div>
                <strong>{{ formatCount(bucket.count) }}</strong>
              </div>
            </div>
          </article>

          <article v-for="group in topN" :key="group.id" class="performance-chart-card performance-top-card">
            <header>
              <div>
                <strong>{{ group.title }}</strong>
                <span>{{ group.subtitle }}</span>
              </div>
            </header>
            <div class="performance-top-list">
              <div v-for="row in group.rows" :key="`${group.id}-${row.label}-${row.detail}`" class="performance-top-row">
                <div>
                  <strong>{{ row.label }}</strong>
                  <span>{{ displayText(row.detail) }}</span>
                </div>
                <div><i :class="toneClass(row.tone)" :style="{ width: `${barPercent(Number(row.value), topMax(group))}%` }" /></div>
                <em>{{ formatValue(row, 'value', row.unit) }}</em>
              </div>
            </div>
          </article>
        </section>
      </template>

      <TablePageLayout title="性能明细" subtitle="来自同一 summary 的性能明细和可访问表格 drill-down">
        <template #actions><button class="table-action" type="button" :disabled="table.loading.value" @click="table.reload">刷新</button></template>
        <DataTable :columns="detailColumns" :rows="detailRows" :row-key="rowKey" empty-text="暂无性能样本。请先打开工作台或执行一次真实页面操作后刷新。">
          <template #cell-metric="{ row }"><strong>{{ metricLabel(row as WebPerformanceRow) }}</strong><small>{{ kindLabel((row as WebPerformanceRow).kind) }}</small></template>
          <template #cell-route="{ row }"><code>{{ displayRoute((row as WebPerformanceRow).route) }}</code><small>{{ formatDimensions(row as WebPerformanceRow) }}</small></template>
          <template #cell-count="{ row }">{{ formatCount((row as WebPerformanceRow).count) }}</template>
          <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>
            <small class="performance-row-contract">{{ rowContractText(row as WebPerformanceRow) }}</small>
            <small class="performance-row-contract">{{ rowWindowText(row as WebPerformanceRow) }}</small>
            <small v-if="backendEvidenceText(row as WebPerformanceRow)" class="performance-row-contract">{{ backendEvidenceText(row as WebPerformanceRow) }}</small>
          </template>
        </DataTable>
      </TablePageLayout>
    </template>
  </section>
</template>
