feat(site-finder): блок «Сети рядом с участком» в §3 (Фаза C2-UI) (#2282)
This commit is contained in:
parent
7c12812f81
commit
8a42fba836
3 changed files with 172 additions and 1 deletions
|
|
@ -15,7 +15,7 @@
|
||||||
* бэка не отработали, показываем нейтральный «загружаются», а не «нет данных».
|
* бэка не отработали, показываем нейтральный «загружаются», а не «нет данных».
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Info, Zap, Droplets, Flame, Thermometer } from "lucide-react";
|
import { Info, Zap, Droplets, Flame, Thermometer, Route } from "lucide-react";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ConnectionCapacityResponse,
|
ConnectionCapacityResponse,
|
||||||
|
|
@ -23,6 +23,7 @@ import type {
|
||||||
GasGrsRow,
|
GasGrsRow,
|
||||||
HeatCapacityBlock,
|
HeatCapacityBlock,
|
||||||
HeatSystemRow,
|
HeatSystemRow,
|
||||||
|
NearbyNetworkZone,
|
||||||
WaterCapacityRow,
|
WaterCapacityRow,
|
||||||
} from "@/hooks/useConnectionCapacity";
|
} from "@/hooks/useConnectionCapacity";
|
||||||
import { CONNECTION_CAPACITY_RADIUS_M } from "@/hooks/useConnectionCapacity";
|
import { CONNECTION_CAPACITY_RADIUS_M } from "@/hooks/useConnectionCapacity";
|
||||||
|
|
@ -34,9 +35,13 @@ import {
|
||||||
formatMva,
|
formatMva,
|
||||||
formatThousandM3Day,
|
formatThousandM3Day,
|
||||||
formatThousandM3Hour,
|
formatThousandM3Hour,
|
||||||
|
formatZoneDistance,
|
||||||
humanizeLoadIndex,
|
humanizeLoadIndex,
|
||||||
|
humanizeNetKind,
|
||||||
|
isThermalNear,
|
||||||
pickCitySystem,
|
pickCitySystem,
|
||||||
pluralizeSystems,
|
pluralizeSystems,
|
||||||
|
sortNearbyNetworkZones,
|
||||||
toFiniteNumber,
|
toFiniteNumber,
|
||||||
} from "@/components/site-finder/connection-capacity";
|
} from "@/components/site-finder/connection-capacity";
|
||||||
|
|
||||||
|
|
@ -494,6 +499,100 @@ function HeatBlock({ heat }: { heat: HeatCapacityBlock | null | undefined }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Сети рядом с участком (охранные зоны, #2119 C2) ────────────────────────────
|
||||||
|
//
|
||||||
|
// Где ФИЗИЧЕСКИ проходит сеть (охранный коридор в радиусе 300 м) — позитив-разрез:
|
||||||
|
// теплотрасса в 24 м → дешёвая врезка. Порядок уже по (distance_m, net_kind) с
|
||||||
|
// бэка; thermal ≤50 м выводим первым и подсвечиваем success-тоном.
|
||||||
|
|
||||||
|
// Маленький нейтральный чип net_kind (тепло/электро/газ/связь/не уточнено).
|
||||||
|
function NetKindChip({ netKind }: { netKind: string }) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
padding: "0 6px",
|
||||||
|
borderRadius: 6,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 500,
|
||||||
|
lineHeight: "18px",
|
||||||
|
color: "var(--fg-secondary)",
|
||||||
|
background: "var(--bg-card-alt)",
|
||||||
|
border: "1px solid var(--border-card)",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{humanizeNetKind(netKind)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NearbyNetworkZoneLine({ zone }: { zone: NearbyNetworkZone }) {
|
||||||
|
const near = isThermalNear(zone);
|
||||||
|
const distance = formatZoneDistance(zone.distance_m);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
gap: 8,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NetKindChip netKind={zone.net_kind} />
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: near ? "var(--success)" : "var(--fg-primary)",
|
||||||
|
fontWeight: near ? 600 : 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{zone.human_label}
|
||||||
|
</span>
|
||||||
|
<StatusBadge
|
||||||
|
label={distance}
|
||||||
|
bg={near ? "var(--success-soft)" : "var(--bg-card-alt)"}
|
||||||
|
fg={near ? "var(--success)" : "var(--fg-secondary)"}
|
||||||
|
border={near ? undefined : "var(--border-card)"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NearbyNetworkZonesBlock({ zones }: { zones: NearbyNetworkZone[] }) {
|
||||||
|
// Пустой массив → блок не рендерится вовсе (не «нет данных»).
|
||||||
|
if (zones.length === 0) return null;
|
||||||
|
|
||||||
|
const ordered = sortNearbyNetworkZones(zones);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={SUB_HEADER}>
|
||||||
|
<Route size={14} strokeWidth={1.5} aria-hidden="true" />
|
||||||
|
Сети рядом с участком
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: "var(--fg-secondary)",
|
||||||
|
marginBottom: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
По охранным зонам — где физически проходит сеть.
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||||
|
{ordered.map((zone, i) => (
|
||||||
|
<NearbyNetworkZoneLine
|
||||||
|
key={`zone-${zone.net_kind}-${zone.distance_m}-${i}`}
|
||||||
|
zone={zone}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Card ───────────────────────────────────────────────────────────────────────
|
// ── Card ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const SUB_HEADER: React.CSSProperties = {
|
const SUB_HEADER: React.CSSProperties = {
|
||||||
|
|
@ -573,6 +672,9 @@ export function ResourceReservesCard({ data }: Props) {
|
||||||
<WaterBlock rows={data.water} />
|
<WaterBlock rows={data.water} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Сети рядом (охранные зоны) — рендерится только при наличии зон */}
|
||||||
|
<NearbyNetworkZonesBlock zones={data.nearby_network_zones} />
|
||||||
|
|
||||||
{/* Caveat */}
|
{/* Caveat */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
GasOutletPoint,
|
GasOutletPoint,
|
||||||
|
NearbyNetworkZone,
|
||||||
PowerConnectionPoint,
|
PowerConnectionPoint,
|
||||||
WaterCapacityRow,
|
WaterCapacityRow,
|
||||||
} from "@/hooks/useConnectionCapacity";
|
} from "@/hooks/useConnectionCapacity";
|
||||||
|
|
@ -257,3 +258,70 @@ export function pluralizeOutletPoints(n: number): string {
|
||||||
return `${n} точки`;
|
return `${n} точки`;
|
||||||
return `${n} точек`;
|
return `${n} точек`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Сети рядом с участком (охранные зоны cad_zouit, #2119 C2) ───────────────────
|
||||||
|
//
|
||||||
|
// nearby_network_zones — где ФИЗИЧЕСКИ проходит сеть (охранный коридор) в радиусе
|
||||||
|
// 300 м. Тепловая зона с малой дистанцией = дешёвая врезка → главный позитив-сигнал
|
||||||
|
// §3. net_kind — Literal бэка (сериализуется как string), нормализуем к known-набору.
|
||||||
|
|
||||||
|
export type NetKind =
|
||||||
|
"thermal" | "electricity" | "gas" | "communication" | "general";
|
||||||
|
|
||||||
|
// Русская метка net_kind (маленький нейтральный чип рядом с human_label).
|
||||||
|
const NET_KIND_LABEL: Record<NetKind, string> = {
|
||||||
|
thermal: "тепло",
|
||||||
|
electricity: "электро",
|
||||||
|
gas: "газ",
|
||||||
|
communication: "связь",
|
||||||
|
general: "не уточнено",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function classifyNetKind(raw: string | null | undefined): NetKind {
|
||||||
|
switch (raw) {
|
||||||
|
case "thermal":
|
||||||
|
case "electricity":
|
||||||
|
case "gas":
|
||||||
|
case "communication":
|
||||||
|
return raw;
|
||||||
|
default:
|
||||||
|
// Бэк отдаёт "general" для неклассифицированных; неизвестное → тоже general.
|
||||||
|
return "general";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function humanizeNetKind(raw: string | null | undefined): string {
|
||||||
|
return NET_KIND_LABEL[classifyNetKind(raw)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Порог «тепло рядом» — thermal с distance_m ≤ этого выводим первым и подсвечиваем
|
||||||
|
// позитивным тоном (дешёвая врезка в теплотрассу). Согласован с C2-логикой §3.
|
||||||
|
export const THERMAL_NEAR_M = 50;
|
||||||
|
|
||||||
|
export function isThermalNear(z: NearbyNetworkZone): boolean {
|
||||||
|
return (
|
||||||
|
classifyNetKind(z.net_kind) === "thermal" && z.distance_m <= THERMAL_NEAR_M
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Дистанция зоны → бейдж. distance_m=0 = зона пересекает участок («по участку»).
|
||||||
|
export function formatZoneDistance(distanceM: number): string {
|
||||||
|
if (distanceM <= 0) return "по участку";
|
||||||
|
return `в ${Math.round(distanceM).toLocaleString("ru-RU")} м`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сортировка для отображения: сначала thermal-near (главный сигнал), затем — исходный
|
||||||
|
// порядок бэка (уже (distance_m, net_kind)). Stable — сохраняет тай-брейки сервера.
|
||||||
|
export function sortNearbyNetworkZones(
|
||||||
|
zones: readonly NearbyNetworkZone[],
|
||||||
|
): NearbyNetworkZone[] {
|
||||||
|
return zones
|
||||||
|
.map((zone, i) => ({ zone, i }))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aNear = isThermalNear(a.zone) ? 0 : 1;
|
||||||
|
const bNear = isThermalNear(b.zone) ? 0 : 1;
|
||||||
|
if (aNear !== bNear) return aNear - bNear;
|
||||||
|
return a.i - b.i;
|
||||||
|
})
|
||||||
|
.map((x) => x.zone);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export type GasGrsRow = components["schemas"]["GasGrsRow"];
|
||||||
export type GasOutletPoint = components["schemas"]["GasOutletPoint"];
|
export type GasOutletPoint = components["schemas"]["GasOutletPoint"];
|
||||||
export type HeatCapacityBlock = components["schemas"]["HeatCapacityBlock"];
|
export type HeatCapacityBlock = components["schemas"]["HeatCapacityBlock"];
|
||||||
export type HeatSystemRow = components["schemas"]["HeatSystemRow"];
|
export type HeatSystemRow = components["schemas"]["HeatSystemRow"];
|
||||||
|
export type NearbyNetworkZone = components["schemas"]["NearbyNetworkZone"];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Радиус поиска центров питания (ЦП, ПС 35/110 кВ) вокруг участка. Дефолт 3 км —
|
* Радиус поиска центров питания (ЦП, ПС 35/110 кВ) вокруг участка. Дефолт 3 км —
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue