feat: add area diagnostics
This commit is contained in:
@@ -1031,6 +1031,7 @@ function AreaDetailPanel({
|
||||
<div className="h-72 rounded-md border border-[var(--border)] p-3">
|
||||
<AreaMetricTrendChart metrics={detail.monthly_metrics} />
|
||||
</div>
|
||||
<AreaDiagnosticsPanel diagnostics={detail.diagnostics} />
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<AreaMetricHistoryTable metrics={detail.monthly_metrics} />
|
||||
<AreaNeighborhoodTable
|
||||
@@ -1051,6 +1052,57 @@ function AreaDetailPanel({
|
||||
);
|
||||
}
|
||||
|
||||
function AreaDiagnosticsPanel({ diagnostics }: { diagnostics: AreaDetail["diagnostics"] }) {
|
||||
if (!diagnostics) {
|
||||
return (
|
||||
<div className="rounded-md border border-[var(--border)] px-4 py-8 text-sm text-[var(--muted-foreground)]">
|
||||
暂无历史分位
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-[var(--border)] p-3">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<p className="text-sm font-semibold text-slate-950">历史分位与异常</p>
|
||||
<Badge variant={diagnostics.anomaly_count > 0 ? "warning" : "success"}>
|
||||
{diagnostics.anomaly_count} 项异常
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
{diagnostics.metrics.map((metric) => (
|
||||
<div key={metric.key} className="rounded-md border border-[var(--border)] bg-white p-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium text-slate-950">{metric.label}</span>
|
||||
<Badge variant={metric.anomaly ? "warning" : "muted"}>{metric.severity}</Badge>
|
||||
</div>
|
||||
<p className="mt-2 text-lg font-semibold text-slate-950">
|
||||
{formatMetricValue(metric.current_value, metric.unit)}
|
||||
</p>
|
||||
<div className="mt-3 h-2 rounded-full bg-[var(--muted)]">
|
||||
<div
|
||||
className={
|
||||
metric.anomaly
|
||||
? "h-2 rounded-full bg-[var(--warning)]"
|
||||
: "h-2 rounded-full bg-[var(--primary)]"
|
||||
}
|
||||
style={{ width: `${Math.max(0, Math.min(100, metric.percentile))}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between text-xs text-[var(--muted-foreground)]">
|
||||
<span>P{formatNumber(metric.percentile, 1)}</span>
|
||||
<span>
|
||||
{formatMetricValue(metric.historical_min, metric.unit)}-
|
||||
{formatMetricValue(metric.historical_max, metric.unit)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AreaMetricTrendChart({ metrics }: { metrics: AreaDetail["monthly_metrics"] }) {
|
||||
const data = [...metrics].reverse();
|
||||
if (data.length === 0) {
|
||||
|
||||
@@ -332,11 +332,33 @@ export type AreaMonthlyMetric = {
|
||||
raw_artifact_id: number | null;
|
||||
};
|
||||
|
||||
export type DiagnosticMetric = {
|
||||
key: string;
|
||||
label: string;
|
||||
unit: string;
|
||||
current_value: number;
|
||||
percentile: number;
|
||||
historical_min: number;
|
||||
historical_max: number;
|
||||
sample_size: number;
|
||||
anomaly: boolean;
|
||||
anomaly_direction: string | null;
|
||||
severity: string;
|
||||
};
|
||||
|
||||
export type AreaDiagnostics = {
|
||||
area_id: string;
|
||||
month: string;
|
||||
metrics: DiagnosticMetric[];
|
||||
anomaly_count: number;
|
||||
};
|
||||
|
||||
export type AreaDetail = {
|
||||
profile: AreaProfile;
|
||||
current_score: AreaScore | null;
|
||||
score_lineage: AreaScoreLineage | null;
|
||||
monthly_metrics: AreaMonthlyMetric[];
|
||||
diagnostics: AreaDiagnostics | null;
|
||||
neighborhood_scores: NeighborhoodScore[];
|
||||
};
|
||||
|
||||
@@ -400,6 +422,15 @@ export async function fetchAreaDetail(areaId: string, month: string): Promise<Ar
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchAreaDiagnostics(
|
||||
areaId: string,
|
||||
month: string,
|
||||
): Promise<AreaDiagnostics> {
|
||||
return fetchJson(
|
||||
`/api/v1/areas/${encodeURIComponent(areaId)}/diagnostics?month=${encodeURIComponent(month)}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchNeighborhoodScores(month: string): Promise<NeighborhoodScore[]> {
|
||||
return fetchJson(`/api/v1/neighborhoods/scores?month=${encodeURIComponent(month)}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user