feat(site-finder): блок «Сети рядом с участком» в §3 (Фаза C2-UI) #2282
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 {
|
||||
ConnectionCapacityResponse,
|
||||
|
|
@ -23,6 +23,7 @@ import type {
|
|||
GasGrsRow,
|
||||
HeatCapacityBlock,
|
||||
HeatSystemRow,
|
||||
NearbyNetworkZone,
|
||||
WaterCapacityRow,
|
||||
} from "@/hooks/useConnectionCapacity";
|
||||
import { CONNECTION_CAPACITY_RADIUS_M } from "@/hooks/useConnectionCapacity";
|
||||
|
|
@ -34,9 +35,13 @@ import {
|
|||
formatMva,
|
||||
formatThousandM3Day,
|
||||
formatThousandM3Hour,
|
||||
formatZoneDistance,
|
||||
humanizeLoadIndex,
|
||||
humanizeNetKind,
|
||||
isThermalNear,
|
||||
pickCitySystem,
|
||||
pluralizeSystems,
|
||||
sortNearbyNetworkZones,
|
||||
toFiniteNumber,
|
||||
} 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 ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
const SUB_HEADER: React.CSSProperties = {
|
||||
|
|
@ -573,6 +672,9 @@ export function ResourceReservesCard({ data }: Props) {
|
|||
<WaterBlock rows={data.water} />
|
||||
</div>
|
||||
|
||||
{/* Сети рядом (охранные зоны) — рендерится только при наличии зон */}
|
||||
<NearbyNetworkZonesBlock zones={data.nearby_network_zones} />
|
||||
|
||||
{/* Caveat */}
|
||||
<div
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import type {
|
||||
GasOutletPoint,
|
||||
NearbyNetworkZone,
|
||||
PowerConnectionPoint,
|
||||
WaterCapacityRow,
|
||||
} from "@/hooks/useConnectionCapacity";
|
||||
|
|
@ -257,3 +258,70 @@ export function pluralizeOutletPoints(n: number): string {
|
|||
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 HeatCapacityBlock = components["schemas"]["HeatCapacityBlock"];
|
||||
export type HeatSystemRow = components["schemas"]["HeatSystemRow"];
|
||||
export type NearbyNetworkZone = components["schemas"]["NearbyNetworkZone"];
|
||||
|
||||
/**
|
||||
* Радиус поиска центров питания (ЦП, ПС 35/110 кВ) вокруг участка. Дефолт 3 км —
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue