From 44fdab2d5ed3f55390b821db1093cffe7c61471f Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 20 Jun 2026 10:14:27 +0300 Subject: [PATCH] =?UTF-8?q?feat(tradein/ui):=20=D0=B4=D0=B2=D0=B5=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BD=D0=BE=D0=BF=D1=80=D0=B0=D0=B2=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20hero-=D1=86=D0=B5=D0=BD=D1=8B=20(=D0=B2=D1=8B=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=B2=D0=B8=D1=82=D1=8C/=D0=BF=D0=BE=D0=BB=D1=83?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D1=8C)=20+=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D0=BD=D1=8B=D0=B9=20=D1=80=D1=8F=D0=B4=20=D0=B1?= =?UTF-8?q?=D0=B5=D0=BD=D1=87=D0=BC=D0=B0=D1=80=D0=BA=D0=BE=D0=B2,=20?= =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D1=82=D1=8C=20=D0=B4=D1=83=D0=B1=D0=BB?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/ui-preview/estimate/page.tsx | 35 ++-- .../src/components/trade-in/HeroSummary.tsx | 125 +++++++++----- .../components/trade-in/SourcesProgress.tsx | 8 - .../src/components/trade-in/trade-in.css | 156 +++++++++++------- 4 files changed, 189 insertions(+), 135 deletions(-) diff --git a/tradein-mvp/frontend/src/app/ui-preview/estimate/page.tsx b/tradein-mvp/frontend/src/app/ui-preview/estimate/page.tsx index 47bdb4bb..5651cacd 100644 --- a/tradein-mvp/frontend/src/app/ui-preview/estimate/page.tsx +++ b/tradein-mvp/frontend/src/app/ui-preview/estimate/page.tsx @@ -20,20 +20,18 @@ * 1. SourcesProgress — estimate prop * 2. WhatIfPanel — baseEstimate + baseInput props * 3. HeroSummary — estimate + input props - * 4. IMVBenchmark — benchmark prop (FIXTURE_IMV, available:true) - * 5. CianValuationCard — data prop - * 6. HouseInfoCard — houses prop (FIXTURE_HOUSES, length>0) - * 7. PlacementHistoryCard — estimateId; seed queryKey "placement-history" - * 8. HouseAnalyticsSection — estimateId; seed "house-analytics" + "sell-time-sensitivity" - * 9. PhotoUpload — estimateId (upload-UI; fetch photo может тихо упасть — ок) - * 10. ListingsCard — estimate + estimateId; seed "cian-price-changes" пустым - * 11. DealsCard — estimate prop - * 12. StreetDealsCard — estimate prop - * 13. MapCard — estimate prop (analogs имеют lat/lon → пины есть) - * 14. DistributionCard — estimate prop - * 15. PriceTrendCard — estimate prop - * 16. ExposureCard — estimate prop - * 17. OfferCard — estimate + brandSlug=null props + * 4. HouseInfoCard — houses prop (FIXTURE_HOUSES, length>0) + * 5. PlacementHistoryCard — estimateId; seed queryKey "placement-history" + * 6. HouseAnalyticsSection — estimateId; seed "house-analytics" + "sell-time-sensitivity" + * 7. PhotoUpload — estimateId (upload-UI; fetch photo может тихо упасть — ок) + * 8. ListingsCard — estimate + estimateId; seed "cian-price-changes" пустым + * 9. DealsCard — estimate prop + * 10. StreetDealsCard — estimate prop + * 11. MapCard — estimate prop (analogs имеют lat/lon → пины есть) + * 12. DistributionCard — estimate prop + * 13. PriceTrendCard — estimate prop + * 14. ExposureCard — estimate prop + * 15. OfferCard — estimate + brandSlug=null props */ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; @@ -41,14 +39,12 @@ import { notFound } from "next/navigation"; import { useState } from "react"; import "@/components/trade-in/trade-in.css"; -import { CianValuationCard } from "@/components/trade-in/CianValuationCard"; import { DealsCard } from "@/components/trade-in/DealsCard"; import { DistributionCard } from "@/components/trade-in/DistributionCard"; import { ExposureCard } from "@/components/trade-in/ExposureCard"; import { HeroSummary } from "@/components/trade-in/HeroSummary"; import { HouseAnalyticsSection } from "@/components/trade-in/HouseAnalyticsSection"; import { HouseInfoCard } from "@/components/trade-in/HouseInfoCard"; -import { IMVBenchmark } from "@/components/trade-in/IMVBenchmark"; import { ListingsCard } from "@/components/trade-in/ListingsCard"; import { OfferCard } from "@/components/trade-in/OfferCard"; import { PhotoUpload } from "@/components/trade-in/PhotoUpload"; @@ -63,7 +59,6 @@ import { FIXTURE_ANALYTICS, FIXTURE_ESTIMATE, FIXTURE_HOUSES, - FIXTURE_IMV, FIXTURE_INPUT, FIXTURE_PLACEMENT, FIXTURE_SALES, @@ -152,12 +147,6 @@ function PreviewContent() { isResubmitting={false} /> - {/* 4. IMVBenchmark — передаём пропом напрямую (available:true → не null) */} - - - {/* 5. CianValuationCard */} - - {/* 6. HouseInfoCard — передаём пропом напрямую (length>0 → не null) */} diff --git a/tradein-mvp/frontend/src/components/trade-in/HeroSummary.tsx b/tradein-mvp/frontend/src/components/trade-in/HeroSummary.tsx index de0cdf71..ece2a03c 100644 --- a/tradein-mvp/frontend/src/components/trade-in/HeroSummary.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/HeroSummary.tsx @@ -295,62 +295,107 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals - {/* ── ЕДИНЫЙ итог: «Ожидаемая цена сделки» (S2). Доминирующий headline. ── - При наличии expected_sold показываем его как главный outcome (ниже - запроса, с −X%). Без него (старые оценки / пустая ratio) — headline = - asking-медиана, чтобы блок не оставался без главного числа. */} -
+ {/* ── Две равноправные hero-цены: «Выставить» (asking) + «Получить» (sold). ── + При hasSold=false (старые оценки / пустая ratio) — одиночный layout + только с «Выставить». Empty-state при insufficientData. */} +
{insufficientData ? ( - <> -
Оценка недоступна
+
+
Оценка недоступна
Недостаточно данных для оценки

Рядом не нашлось достаточно сопоставимых объявлений и сделок, чтобы рассчитать цену. Уточните адрес или параметры объекта и попробуйте снова.

- +
) : ( <> -
- {hasSold ? "Ожидаемая цена сделки" : "Рекомендованная цена"} - {showDiscount && ( - - −{discountPct}% к объявлению - - )} -
-
- {formatMln(hasSold ? (sold as number) : m)} ₽ -
- {hasSold && typeof soldLo === "number" && typeof soldHi === "number" && ( -
- ожидаемый диапазон {formatMln(soldLo)} – {formatMln(soldHi)} ₽ - {typeof soldPerM2 === "number" - ? ` · ${soldPerM2.toLocaleString("ru-RU")} ₽/м²` - : ""} + {/* Левая ячейка: «Выставить в объявлении» */} +
+
Выставить в объявлении
+
{formatMln(m)} ₽
+
+ {formatMln(lo)} – {formatMln(hi)} ₽
- )} +
+ {estimate.median_price_per_m2.toLocaleString("ru-RU")} ₽/м² +
+
+ + {/* Правая ячейка: «Получить (ожидаемая сделка)» — только при hasSold */} {hasSold && ( -

- Реальные сделки проходят на 5–12% ниже цен в объявлениях — это - ожидаемая цена сделки по данным ДКП Росреестра, а не цена запроса. -

- )} - {hasSold && estimate.ratio_basis === "global_fallback" && ( -

- Вероятная цена продажи рассчитана по средне-городскому коэффициенту — - мало зарегистрированных сделок в данном сегменте комнатности. Оценка - ориентировочная. -

+
+
+ Получить (ожидаемая сделка) + {showDiscount && ( + + −{discountPct}% + + )} +
+
{formatMln(sold as number)} ₽
+ {typeof soldLo === "number" && typeof soldHi === "number" && ( +
+ {formatMln(soldLo)} – {formatMln(soldHi)} ₽ +
+ )} + {typeof soldPerM2 === "number" && ( +
+ {soldPerM2.toLocaleString("ru-RU")} ₽/м² +
+ )} + {estimate.ratio_basis === "global_fallback" && ( +
+ Ср.-городской коэффициент — мало сделок в сегменте +
+ )} +
)} )}
+ {/* ── Компактный ряд бенчмарков: Avito IMV · Cian · ДКП ── + Вторичный контекст-якорь под hero, не конкурирует по размеру. */} + {!insufficientData && (imvPrice !== null || estimate.cian_valuation?.sale_price_rub != null || showDkp) && ( +
+ {imvPrice !== null && ( +
+ Avito IMV + {formatMln(imvPrice)} ₽ +
+ )} + {estimate.cian_valuation?.sale_price_rub != null && ( +
+ Cian + + {formatMln(estimate.cian_valuation.sale_price_rub)} ₽ + + {estimate.cian_valuation.rent_price_rub != null && ( + + аренда {(estimate.cian_valuation.rent_price_rub / 1000).toFixed(0)} т.р./мес + + )} +
+ )} + {showDkp && dkp && ( +
+ ДКП · Росреестр + + {formatMln(dkp.median_ppm2 * dkpArea)} ₽ + + + {dkp.median_ppm2.toLocaleString("ru-RU")} ₽/м² · {dkp.count} сд. + +
+ )} +
+ )} + {/* ── Доверие + действия (web-преимущества над PDF): свежесть данных, collapsible «Как рассчитано», PDF-отчёт, CTA трейд-ин. Headline выше остаётся чистым — прозрачность вторична/collapsible. ── */} diff --git a/tradein-mvp/frontend/src/components/trade-in/SourcesProgress.tsx b/tradein-mvp/frontend/src/components/trade-in/SourcesProgress.tsx index adbeb289..c6f0e8c0 100644 --- a/tradein-mvp/frontend/src/components/trade-in/SourcesProgress.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/SourcesProgress.tsx @@ -114,14 +114,6 @@ export function SourcesProgress({ estimate, isPending }: Props) { {doneCount} / {totalCount} источников
- {estimate && ( -
- медиана:{" "} - - {estimate.insufficient_data ? "—" : `${formatMln(estimate.median_price_rub)} ₽`} - -
- )}
diff --git a/tradein-mvp/frontend/src/components/trade-in/trade-in.css b/tradein-mvp/frontend/src/components/trade-in/trade-in.css index 67c32c15..1cdc3fc9 100644 --- a/tradein-mvp/frontend/src/components/trade-in/trade-in.css +++ b/tradein-mvp/frontend/src/components/trade-in/trade-in.css @@ -689,54 +689,117 @@ border-color: var(--border-strong, var(--border)); } - /* ── Единый headline-итог: «Ожидаемая цена сделки» (#648 IA redesign) ── - Доминирует над двумя reference-figure ниже: больший размер value, - акцентный фон. Снимает «два одинаковых числа» — это ОДНО главное число. */ - .hero-headline { - padding: 20px 22px 16px; + /* ── Две равноправные hero-цены: «Выставить» + «Получить» ── + Равный визуальный вес: одинаковый размер value у обоих. */ + .hero-duo { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + padding: 20px 22px; + border-bottom: 1px solid var(--border); + } + .hero-duo--single { grid-template-columns: 1fr; } + .hero-duo__cell { display: flex; flex-direction: column; - gap: 6px; - background: var(--accent-soft); - border-bottom: 1px solid color-mix(in oklch, var(--accent) 18%, var(--border)); + gap: 5px; + padding: 18px 20px; + background: var(--surface-2); + border: 1px solid var(--border); + border-radius: var(--radius); } - .headline-label { + .hero-duo__cell[data-role=sold] { + background: var(--accent-soft); + border-color: color-mix(in oklch, var(--accent) 22%, var(--border)); + } + .hero-duo__label { display: flex; align-items: center; - gap: 10px; + gap: 8px; flex-wrap: wrap; - font-size: 13px; - font-weight: 600; - letter-spacing: 0.01em; - color: var(--accent-ink); - } - .headline-delta { - font-family: var(--font-mono); font-size: 12px; font-weight: 600; - padding: 2px 8px; + letter-spacing: 0.02em; + color: var(--muted); + } + .hero-duo__cell[data-role=sold] .hero-duo__label { color: var(--accent-ink); } + .hero-duo__delta { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 600; + padding: 1px 6px; border-radius: var(--radius-sm); background: color-mix(in oklch, var(--accent) 16%, transparent); color: var(--accent-ink); } - .headline-value { - font-size: 38px; + .hero-duo__value { + font-size: 28px; font-weight: 700; - line-height: 1.05; - letter-spacing: -0.025em; + line-height: 1.1; + letter-spacing: -0.02em; color: var(--fg); } + .hero-duo__range { + font-size: 12px; + color: var(--muted); + letter-spacing: 0.01em; + } + .hero-duo__perm2 { + font-size: 11px; + color: var(--muted-2); + letter-spacing: 0.04em; + } + .hero-duo__note { + margin-top: 4px; + font-size: 11px; + color: var(--muted); + font-style: italic; + } + + /* ── Компактный ряд бенчмарков под hero-duo ── */ + .hero-benchmarks { + display: flex; + flex-wrap: wrap; + gap: 10px; + padding: 14px 22px; + border-bottom: 1px solid var(--border); + background: var(--surface-1, var(--bg)); + } + .bench-chip { + display: flex; + flex-direction: column; + gap: 2px; + padding: 8px 12px; + background: var(--surface-2); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + min-width: 120px; + } + .bench-chip__src { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--muted-2); + } + .bench-chip__val { + font-size: 14px; + font-weight: 600; + letter-spacing: -0.01em; + color: var(--fg); + } + .bench-chip__sub { + font-size: 10px; + color: var(--muted); + } + + /* ── empty-state headline (без данных) ── */ .headline-value-empty { font-size: 22px; font-weight: 600; line-height: 1.27; color: var(--fg); } - .headline-range { - font-size: 12px; - color: var(--muted); - letter-spacing: 0.01em; - } .headline-note { margin: 4px 0 0; font-size: 12px; @@ -745,43 +808,7 @@ max-width: 60ch; } .headline-note b { font-weight: 700; } - .price-figure-label { - display: flex; - align-items: center; - gap: 8px; - font-size: 12px; - font-weight: 500; - letter-spacing: 0.02em; - color: var(--muted); - } - .price-figure[data-kind="sold"] .price-figure-label { color: var(--accent-ink); } - .price-figure-delta { - font-family: var(--font-mono); - font-size: 11px; - font-weight: 600; - letter-spacing: 0.02em; - padding: 1px 6px; - border-radius: var(--radius-sm); - background: color-mix(in oklch, var(--accent) 14%, transparent); - color: var(--accent-ink); - } - .price-figure-value { - font-size: 28px; - font-weight: 600; - line-height: 1.1; - letter-spacing: -0.02em; - color: var(--fg); - } - .price-figure-range { - font-size: 12px; - color: var(--muted); - letter-spacing: 0.01em; - } - .price-figure-perm2 { - font-size: 11px; - color: var(--muted-2); - letter-spacing: 0.04em; - } + .hero-prices-note { margin: 0; padding: 12px 22px 0; @@ -1377,6 +1404,7 @@ .layout { grid-template-columns: 1fr; } .form-card { position: static; max-height: none; } .hero-top { grid-template-columns: 1fr; } + .hero-duo { grid-template-columns: 1fr; } .hero-prices { grid-template-columns: 1fr; } .hero-photo { aspect-ratio: 16/9; } .progress-list { grid-template-columns: 1fr; } -- 2.45.3