fix(trade-in/ui): расшифровка ДКП/IMV/CV + графики на --viz токены + убрать emoji (#1871) #1922
8 changed files with 51 additions and 27 deletions
|
|
@ -191,7 +191,19 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
||||||
lineHeight: 1.4,
|
lineHeight: 1.4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
⚠️ Адрес распознан неточно — оценка может быть приближённой.
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: "var(--warn)",
|
||||||
|
marginRight: 8,
|
||||||
|
verticalAlign: "middle",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Адрес распознан неточно — оценка может быть приближённой.
|
||||||
Уточните адрес и пересчитайте для более точного результата.
|
Уточните адрес и пересчитайте для более точного результата.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -208,7 +220,7 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
||||||
</b>
|
</b>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 4 }}>
|
<div style={{ marginTop: 4 }}>
|
||||||
Достоверность · <b style={{ color: conf.color }}>{conf.txt}</b> · CV {cv.toFixed(1)}%
|
Достоверность · <b style={{ color: conf.color }}>{conf.txt}</b> · <abbr title="Коэффициент вариации (разброс цен аналогов)">CV</abbr> {cv.toFixed(1)}%
|
||||||
{precisionBadge && (
|
{precisionBadge && (
|
||||||
<span className={precisionBadge.className} title="Точность определения адреса">
|
<span className={precisionBadge.className} title="Точность определения адреса">
|
||||||
{precisionBadge.text}
|
{precisionBadge.text}
|
||||||
|
|
@ -376,7 +388,7 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
||||||
<div className="hero-benchmarks">
|
<div className="hero-benchmarks">
|
||||||
{imvPrice !== null && (
|
{imvPrice !== null && (
|
||||||
<div className="bench-chip">
|
<div className="bench-chip">
|
||||||
<span className="bench-chip__src">Avito IMV</span>
|
<span className="bench-chip__src">Avito <abbr title="Information Market Value — оценка рыночной стоимости от Avito">IMV</abbr></span>
|
||||||
<span className="bench-chip__val mono">{formatMln(imvPrice)} ₽</span>
|
<span className="bench-chip__val mono">{formatMln(imvPrice)} ₽</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -395,7 +407,7 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
||||||
)}
|
)}
|
||||||
{showDkp && dkp && (
|
{showDkp && dkp && (
|
||||||
<div className="bench-chip">
|
<div className="bench-chip">
|
||||||
<span className="bench-chip__src">ДКП · Росреестр</span>
|
<span className="bench-chip__src"><abbr title="Договор купли-продажи (реальные сделки Росреестра)">ДКП</abbr> · Росреестр</span>
|
||||||
<span className="bench-chip__val mono">
|
<span className="bench-chip__val mono">
|
||||||
{formatMln(dkp.median_ppm2 * dkpArea)} ₽
|
{formatMln(dkp.median_ppm2 * dkpArea)} ₽
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -438,7 +450,7 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
||||||
title="Индекс Market Value Avito по этому дому — реальный рыночный якорь"
|
title="Индекс Market Value Avito по этому дому — реальный рыночный якорь"
|
||||||
style={{ marginTop: 6, fontSize: 12, color: "var(--muted)" }}
|
style={{ marginTop: 6, fontSize: 12, color: "var(--muted)" }}
|
||||||
>
|
>
|
||||||
Avito IMV: {formatMln(imvPrice)} ₽
|
Avito <abbr title="Information Market Value — оценка рыночной стоимости от Avito">IMV</abbr>: {formatMln(imvPrice)} ₽
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -446,7 +458,7 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
||||||
{showDkp && dkp && (
|
{showDkp && dkp && (
|
||||||
<div className="price-figure" data-kind="dkp">
|
<div className="price-figure" data-kind="dkp">
|
||||||
<div className="price-figure-label">
|
<div className="price-figure-label">
|
||||||
Цены по фактическим сделкам (ДКП · Росреестр)
|
Цены по фактическим сделкам (<abbr title="Договор купли-продажи (реальные сделки Росреестра)">ДКП</abbr> · Росреестр)
|
||||||
</div>
|
</div>
|
||||||
<div className="price-figure-value mono">
|
<div className="price-figure-value mono">
|
||||||
{formatMln(dkp.median_ppm2 * dkpArea)} ₽
|
{formatMln(dkp.median_ppm2 * dkpArea)} ₽
|
||||||
|
|
@ -455,7 +467,7 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
||||||
{formatMln(dkp.low_ppm2 * dkpArea)} – {formatMln(dkp.high_ppm2 * dkpArea)} ₽
|
{formatMln(dkp.low_ppm2 * dkpArea)} – {formatMln(dkp.high_ppm2 * dkpArea)} ₽
|
||||||
</div>
|
</div>
|
||||||
<div className="price-figure-perm2 mono">
|
<div className="price-figure-perm2 mono">
|
||||||
{dkp.median_ppm2.toLocaleString("ru-RU")} ₽/м² · {dkp.count} ДКП за{" "}
|
{dkp.median_ppm2.toLocaleString("ru-RU")} ₽/м² · {dkp.count} <abbr title="Договор купли-продажи (реальные сделки Росреестра)">ДКП</abbr> за{" "}
|
||||||
{dkp.period_months} мес
|
{dkp.period_months} мес
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -641,7 +653,7 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<b>Цены в объявлениях — это ожидания собственников.</b> Реальные сделки
|
<b>Цены в объявлениях — это ожидания собственников.</b> Реальные сделки
|
||||||
проходят на <b>5–12% ниже</b> по данным ДКП Росреестра.
|
проходят на <b>5–12% ниже</b> по данным <abbr title="Договор купли-продажи (реальные сделки Росреестра)">ДКП</abbr> Росреестра.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="warn-row">
|
<div className="warn-row">
|
||||||
|
|
|
||||||
|
|
@ -253,9 +253,20 @@ export function HeroTransparency({ estimate, brandSlug, brandName }: Props) {
|
||||||
{showPrecisionWarning && (
|
{showPrecisionWarning && (
|
||||||
<span
|
<span
|
||||||
aria-label="Предупреждение: адрес определён приблизительно при низкой достоверности"
|
aria-label="Предупреждение: адрес определён приблизительно при низкой достоверности"
|
||||||
style={{ marginLeft: 6 }}
|
style={{ marginLeft: 6, display: "inline-flex", alignItems: "center", gap: 4 }}
|
||||||
>
|
>
|
||||||
⚠️
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
borderRadius: "50%",
|
||||||
|
background: "var(--warn)",
|
||||||
|
verticalAlign: "middle",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: 11, color: "var(--warn)", fontWeight: 600 }}>Приблизительно</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ export function IMVBenchmark({ benchmark, isLoading }: Props) {
|
||||||
return (
|
return (
|
||||||
<section className="imv-benchmark" aria-label="Avito IMV benchmark">
|
<section className="imv-benchmark" aria-label="Avito IMV benchmark">
|
||||||
<header className="imv-benchmark__head">
|
<header className="imv-benchmark__head">
|
||||||
<span className="imv-benchmark__title">Avito IMV</span>
|
<span className="imv-benchmark__title">Avito <abbr title="Information Market Value — оценка рыночной стоимости от Avito">IMV</abbr></span>
|
||||||
<a
|
<a
|
||||||
href="https://www.avito.ru/evaluation/realty"
|
href="https://www.avito.ru/evaluation/realty"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,11 @@ const LEAFLET_JS = `https://unpkg.com/leaflet@${LEAFLET_VER}/dist/leaflet.js`;
|
||||||
const LEAFLET_CSS_SRI = "sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=";
|
const LEAFLET_CSS_SRI = "sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=";
|
||||||
const LEAFLET_JS_SRI = "sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=";
|
const LEAFLET_JS_SRI = "sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=";
|
||||||
|
|
||||||
// Цвета пинов: asking (синий) / ДКП (зелёный) / target (оранжевый акцент).
|
// Цвета пинов: asking (синий --viz-1) / ДКП (бирюза --viz-3) / target (янтарь --viz-4).
|
||||||
const COLOR_ANALOG = "#2563eb";
|
// Leaflet принимает строку-цвет напрямую (не CSS vars) — используем hex-значения токенов.
|
||||||
const COLOR_DEAL = "#16a34a";
|
const COLOR_ANALOG = "#1d4ed8"; // --viz-1
|
||||||
const COLOR_TARGET = "#ea580c";
|
const COLOR_DEAL = "#14b8a6"; // --viz-3
|
||||||
|
const COLOR_TARGET = "#f59e0b"; // --viz-4
|
||||||
|
|
||||||
/** Подгружает Leaflet с CDN один раз, резолвит window.L. (mirror MapPicker) */
|
/** Подгружает Leaflet с CDN один раз, резолвит window.L. (mirror MapPicker) */
|
||||||
function loadLeaflet(): Promise<any> {
|
function loadLeaflet(): Promise<any> {
|
||||||
|
|
|
||||||
|
|
@ -130,9 +130,9 @@ export function MapPicker({ onPick, onClose }: Props) {
|
||||||
else
|
else
|
||||||
marker = L.circleMarker([lat, lon], {
|
marker = L.circleMarker([lat, lon], {
|
||||||
radius: 8,
|
radius: 8,
|
||||||
color: "#1d4ed8",
|
color: "#1d4ed8", // --viz-1 hex (Leaflet не принимает CSS vars)
|
||||||
weight: 2,
|
weight: 2,
|
||||||
fillColor: "#1d4ed8",
|
fillColor: "#1d4ed8", // --viz-1 hex
|
||||||
fillOpacity: 0.45,
|
fillOpacity: 0.45,
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
setAddress(null);
|
setAddress(null);
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ export function PriceHistoryChart({ points }: Props) {
|
||||||
<Line
|
<Line
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="avito_imv"
|
dataKey="avito_imv"
|
||||||
stroke="#2563eb"
|
stroke="var(--viz-1)"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
dot={{ r: 3 }}
|
dot={{ r: 3 }}
|
||||||
name="Avito"
|
name="Avito"
|
||||||
|
|
@ -88,7 +88,7 @@ export function PriceHistoryChart({ points }: Props) {
|
||||||
<Line
|
<Line
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="yandex_valuation"
|
dataKey="yandex_valuation"
|
||||||
stroke="#ea580c"
|
stroke="var(--viz-4)"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
dot={{ r: 3 }}
|
dot={{ r: 3 }}
|
||||||
name="Яндекс"
|
name="Яндекс"
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export function PriceRangeBar({ rangeLow, rangeHigh, median }: Props) {
|
||||||
position: "relative",
|
position: "relative",
|
||||||
height: 10,
|
height: 10,
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
background: "linear-gradient(to right, #bfdbfe, #1d4ed8)",
|
background: "linear-gradient(to right, color-mix(in srgb, var(--viz-1) 30%, white), var(--viz-1))",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Median marker */}
|
{/* Median marker */}
|
||||||
|
|
@ -44,7 +44,7 @@ export function PriceRangeBar({ rangeLow, rangeHigh, median }: Props) {
|
||||||
width: 14,
|
width: 14,
|
||||||
height: 14,
|
height: 14,
|
||||||
borderRadius: "50%",
|
borderRadius: "50%",
|
||||||
background: "#1d4ed8",
|
background: "var(--viz-1)",
|
||||||
border: "2px solid #fff",
|
border: "2px solid #fff",
|
||||||
boxShadow: "0 1px 4px rgba(0,0,0,0.25)",
|
boxShadow: "0 1px 4px rgba(0,0,0,0.25)",
|
||||||
}}
|
}}
|
||||||
|
|
@ -65,7 +65,7 @@ export function PriceRangeBar({ rangeLow, rangeHigh, median }: Props) {
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: "#1d4ed8",
|
color: "var(--viz-1)",
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@ const BUCKET_LABELS: Record<string, string> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const BUCKET_COLORS: Record<string, string> = {
|
const BUCKET_COLORS: Record<string, string> = {
|
||||||
cheap: "#dcfce7", // light green
|
cheap: "var(--success-soft)", // light green
|
||||||
median: "#dbeafe", // light blue
|
median: "var(--accent-soft)", // light blue
|
||||||
plus5: "#fef3c7", // light yellow
|
plus5: "var(--warn-soft)", // light yellow
|
||||||
plus10: "#fee2e2", // light red
|
plus10: "var(--danger-soft)", // light red
|
||||||
};
|
};
|
||||||
|
|
||||||
function BucketCard({ bucket }: { bucket: SellTimeBucket }) {
|
function BucketCard({ bucket }: { bucket: SellTimeBucket }) {
|
||||||
|
|
@ -30,7 +30,7 @@ function BucketCard({ bucket }: { bucket: SellTimeBucket }) {
|
||||||
background: BUCKET_COLORS[bucket.price_premium_label] ?? "#f9fafb",
|
background: BUCKET_COLORS[bucket.price_premium_label] ?? "#f9fafb",
|
||||||
border:
|
border:
|
||||||
bucket.price_premium_label === "median"
|
bucket.price_premium_label === "median"
|
||||||
? "2px solid #2563eb"
|
? "2px solid var(--viz-1)"
|
||||||
: "1px solid var(--border, #e5e7eb)",
|
: "1px solid var(--border, #e5e7eb)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue