From d7be644aed178adef21b491772d22479156c11c1 Mon Sep 17 00:00:00 2001 From: wasrusgen Date: Mon, 11 May 2026 14:56:41 +0300 Subject: [PATCH] miniapp: price comparison matrix as PRIMARY view per category MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WHAT CHANGED: - New _renderPriceMatrix(models) — table with rows=models, columns=stores - Inserted as PRIMARY view above model cards (was secondary accordion) - Columns dynamically include only stores that returned data - Sticky model column (left) — scrolls horizontally on mobile - Best price per row highlighted: green bg + ✓ badge + green text - Empty cells: '—' if no URL, 'смотреть →' if URL but no price yet - 'Мин' column on far right — explicit cheapest price summary CSS: - .report-matrix-wrap with rounded card - Sticky col-model with box-shadow on right edge - Cell-price.best with rgba green background - .best-mark circle badge PREVIEW: - Updated mock with 3 fridges + 3 hobs across multiple stores (real pricing spread) - Demonstrates min-price highlighting working UX: - User can now visually compare 'where is it cheapest' at a glance - Tap any cell with price → opens store page - Tap empty cell with URL → opens search in store NEXT: same matrix can become PDF/Excel export for client briefcase --- miniapp/assets/podbor.css | 144 +++++++++++++++++++++++++++++- miniapp/assets/podbor.js | 101 +++++++++++++++++++-- miniapp/index.html | 14 +-- miniapp/preview-report.html | 171 +++++++++++++++++++++++++++++------- 4 files changed, 381 insertions(+), 49 deletions(-) diff --git a/miniapp/assets/podbor.css b/miniapp/assets/podbor.css index 5f88e83..664a867 100644 --- a/miniapp/assets/podbor.css +++ b/miniapp/assets/podbor.css @@ -1392,7 +1392,149 @@ } .report-link:active { background: var(--warm); } -/* Сравнительная таблица — accordion */ +/* ============================================================ + Матрица цен по магазинам (PRIMARY view категории) + ============================================================ */ + +.report-matrix-wrap { + background: #fff; + border: 1px solid var(--line); + border-radius: 14px; + overflow: hidden; + margin: 8px 0 16px; +} + +.report-matrix-head { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--muted); + padding: 12px 14px 8px; +} + +.report-matrix-scroll { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: thin; +} + +.report-matrix { + width: 100%; + border-collapse: collapse; + font-size: 12.5px; + min-width: 100%; +} + +.report-matrix thead th { + background: var(--warm); + font-family: var(--font-mono); + font-size: 9.5px; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--muted); + padding: 10px 12px; + text-align: left; + border-bottom: 1px solid var(--line); + white-space: nowrap; + position: relative; +} + +.report-matrix .col-model { + position: sticky; + left: 0; + background: var(--warm); + z-index: 2; + min-width: 130px; + box-shadow: 2px 0 4px rgba(31, 26, 20, 0.06); +} +.report-matrix tbody .col-model { + background: #FFFCF6; +} + +.report-matrix tbody td { + padding: 10px 12px; + border-top: 1px solid var(--line); + vertical-align: middle; + white-space: nowrap; +} + +.report-matrix .m-brand { + font-family: var(--font-mono); + font-size: 9px; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--muted); + margin-bottom: 2px; +} +.report-matrix .m-name { + font-family: var(--font-sans); + font-size: 12.5px; + font-weight: 600; + color: var(--ink); + white-space: normal; + line-height: 1.25; +} + +.report-matrix .cell-price { + font-size: 12.5px; + color: var(--ink); +} +.report-matrix .cell-price a { + color: var(--ink); + text-decoration: none; + border-bottom: 1px dashed transparent; + transition: border 0.12s; +} +.report-matrix .cell-price a:active { border-bottom-color: var(--accent-2); } +.report-matrix .cell-price.empty { color: var(--muted); text-align: center; } +.report-matrix .cell-price.best { + background: rgba(42, 107, 63, 0.10); +} +.report-matrix .cell-price.best a, +.report-matrix .cell-price.best strong { + color: #1F5530; +} +.report-matrix .best-mark { + display: inline-block; + width: 16px; + height: 16px; + background: #2A6B3F; + color: #fff; + border-radius: 50%; + text-align: center; + font-size: 10px; + font-weight: 700; + line-height: 16px; + margin-left: 4px; + vertical-align: middle; +} +.cell-noprice-link { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.06em; + color: var(--accent-2); +} + +.report-matrix .col-best { + font-size: 12.5px; + color: var(--ink); + font-weight: 600; + background: #FFFCF6; +} + +.report-matrix-hint { + font-size: 11.5px; + color: var(--muted); + font-style: italic; + padding: 10px 14px 14px; + border-top: 1px dashed var(--line); +} + +/* Сравнительная таблица — accordion (legacy, оставлен для совместимости) */ .report-compare { background: #fff; border: 1px solid var(--line); diff --git a/miniapp/assets/podbor.js b/miniapp/assets/podbor.js index 47cf178..1d1d05a 100644 --- a/miniapp/assets/podbor.js +++ b/miniapp/assets/podbor.js @@ -1265,19 +1265,19 @@ const Podbor = (function () { ${_esc(catLabel)} ${catAnalysis ? `
${_esc(catAnalysis)}
` : ""} -
`); - const modelsWrap = catNode.querySelector(".report-models"); + // Сравнение цен — основной блок, всегда вверху + const matrixNode = _renderPriceMatrix(models); + if (matrixNode) catNode.appendChild(matrixNode); + + // Детальные карточки с pros/cons + const modelsWrap = el(`
`); for (const m of models) { modelsWrap.appendChild(_renderModelCard(m)); } - - // Сравнение - if (models.length >= 2) { - modelsWrap.appendChild(_renderCompareTable(models)); - } + catNode.appendChild(modelsWrap); wrap.appendChild(catNode); } @@ -1384,6 +1384,93 @@ const Podbor = (function () { return "магазинов"; } + /* Матрица цен: rows = models, cols = магазины, cells = цены, лучшая подсвечена */ + function _renderPriceMatrix(models) { + if (!models || !models.length) return null; + + const STORES = [ + { key: "ozon", label: "OZON" }, + { key: "citilink", label: "Citilink" }, + { key: "wb", label: "Wildberries" }, + { key: "yamarket", label: "Я.Маркет" }, + { key: "dns", label: "DNS" }, + ]; + + // Определяем какие магазины имеют хоть какую-то цену — только их колонки + const activeStores = STORES.filter(s => + models.some(m => (m.enriched || {})[s.key] && ((m.enriched || {})[s.key].price_min_rub || (m.enriched || {})[s.key].url)) + ); + + // Если ни один store не дал цены — показываем простую таблицу с AI-ценами + const showStoresCols = activeStores.length > 0; + + const head = ` + + + Модель + ${showStoresCols + ? activeStores.map(s => `${s.label}`).join("") + : `Цена (AI)`} + Мин + + + `; + + const rows = models.map(m => { + const enriched = m.enriched || {}; + // Собираем цены по каждому активному магазину + const cellPrices = showStoresCols + ? activeStores.map(s => { + const item = enriched[s.key]; + return { + store: s.key, + price: item && item.price_min_rub, + url: item && item.url, + }; + }) + : [{ store: "ai", price: enriched.price_min_rub || m.price_min_rub, url: enriched.best_url || null }]; + + const validPrices = cellPrices.map(c => c.price).filter(p => p && p > 0); + const bestPrice = validPrices.length ? Math.min(...validPrices) : null; + + const modelCell = ` + +
${_esc(m.brand || "")}
+
${_esc(m.model || "")}
+ + `; + const storeCells = cellPrices.map(c => { + if (!c.price) { + return c.url + ? `смотреть →` + : `—`; + } + const isBest = bestPrice && c.price === bestPrice; + const priceHtml = `${formatRub(c.price)} ₽`; + const cellInner = c.url + ? `${priceHtml}${isBest ? ' ' : ''}` + : `${priceHtml}${isBest ? ' ' : ''}`; + return `${cellInner}`; + }).join(""); + const bestCell = `${bestPrice ? `${formatRub(bestPrice)}` : "—"}`; + + return `${modelCell}${storeCells}${bestCell}`; + }).join(""); + + return el(` +
+
Цены по магазинам — лучшая отмечена ✓
+
+ + ${head} + ${rows} +
+
+ ${!showStoresCols ? `
Магазины ещё не нашли товар. Цены — оценка AI на основе текущих рыночных данных.
` : ""} +
+ `); + } + function _renderCompareTable(models) { const rows = models.map(m => { const e = m.enriched || {}; diff --git a/miniapp/index.html b/miniapp/index.html index ed41367..23e3bb7 100644 --- a/miniapp/index.html +++ b/miniapp/index.html @@ -12,8 +12,8 @@ - - + +
@@ -21,10 +21,10 @@
- - - - - + + + + + diff --git a/miniapp/preview-report.html b/miniapp/preview-report.html index 189c30d..b0fdc21 100644 --- a/miniapp/preview-report.html +++ b/miniapp/preview-report.html @@ -76,60 +76,89 @@ const MOCK_AI = { }, { brand: "Liebherr", model: "CNd 5223", - price_min_rub: 109900, price_max_rub: 124000, + price_min_rub: 134900, price_max_rub: 154000, highlights: ["BioFresh (зона свежести)", "NoFrost", "SoftClose"], - pros: ["премиум-качество сборки", "очень тихий 34дБ", "10 лет гарантии на компрессор"], - cons: ["цена выше среднего на +15%"], + pros: ["премиум-качество немецкой сборки", "очень тихий 34 дБ — на 2 дБ тише Haier", "BioFresh — овощи дольше хрустящие на 30 дней", "10 лет гарантии на компрессор"], + cons: ["цена выше Haier на ~50% при том же объёме", "идёт параллельным импортом — ждать 4-6 недель"], + reasoning: "Премиум-выбор для тех, кому важна зона свежести и тишина. Переплата ~50% за бренд и BioFresh.", tier: "premium", enriched: { image_url: "https://placehold.co/200x200/EFE3CC/6B4A2B?text=Liebherr", - price_min_rub: 109900, price_max_rub: 124000, - rating_max: 4.9, reviews_total: 873, stores_count: 8, - wb: { url: "#" }, yamarket: { url: "#" } + rating_max: 4.9, reviews_total: 873, stores_count: 4, + ozon: { url: "https://www.ozon.ru/product/liebherr-cnd5223/", price_min_rub: 134900 }, + citilink: { url: "https://www.citilink.ru/product/liebherr-cnd5223/", price_min_rub: 142000 }, + yamarket: { url: "https://market.yandex.ru/product--liebherr-cnd5223/", price_min_rub: 154000 } } }, { - brand: "Indesit", model: "ITS 4180 W", + brand: "Бирюса", model: "M124", price_min_rub: 39990, highlights: ["LowFrost (размораживать только мороз)", "класс A+"], - pros: ["доступная цена", "компактные габариты"], - cons: ["без инвертора", "шум 42дБ"], + pros: ["доступная цена в 2 раза ниже Haier", "компактные габариты 600×600×1900 мм — идеальная ниша", "официальная гарантия 3 года от российского производителя"], + cons: ["без инвертора — компрессор работает циклами, заметнее на слух", "шум 42 дБ — на 6 дБ громче Haier", "нет зон свежести"], + reasoning: "Страховочный бюджет-вариант если клиент не хочет переплачивать. Российский, доступный, надёжный.", tier: "budget", enriched: { - image_url: "https://placehold.co/200x200/FBF7F0/6B4A2B?text=Indesit", - price_min_rub: 39990, price_max_rub: 44900, - rating_max: 4.3, reviews_total: 2100, stores_count: 15, - dns: { url: "#" }, ozon: { url: "#" } + image_url: "https://placehold.co/200x200/FBF7F0/6B4A2B?text=Birusa", + rating_max: 4.3, reviews_total: 2100, stores_count: 5, + ozon: { url: "https://www.ozon.ru/product/biryusa-m124/", price_min_rub: 39990 }, + citilink: { url: "https://www.citilink.ru/product/biryusa-m124/", price_min_rub: 41500 }, + wb: { url: "https://www.wildberries.ru/catalog/biryusa-m124/", price_min_rub: 42990 }, + dns: { url: "https://www.dns-shop.ru/product/biryusa-m124/", price_min_rub: 44900 } } } ] }, hob: { + analysis: "В 2026 году индукционные варочные среднего сегмента — это Haier и Korting. Bosch ещё доступен через ПИ, но цена выше на 25%. Все три модели — 60 см, 4 зоны, индукция.", models: [ { - brand: "Bosch", model: "PUE611BB5E", - price_min_rub: 59900, price_max_rub: 68000, - highlights: ["Индукция", "PowerBoost (форсаж — кипятит за минуту)", "TouchSelect (плавная регулировка)"], - pros: ["4 индукционные зоны", "безопасность для детей", "класс A"], - cons: ["требует индукционной посуды"], + brand: "Haier", label: "Haier", + model: "HHX-Y64NFB", + price_min_rub: 39990, price_max_rub: 48000, + highlights: ["Индукция (магнитный нагрев посуды)", "Booster (кипятит за минуту)", "Сенсорное управление"], + pros: ["4 индукционные зоны 60 см — стандарт", "Booster на 2 зонах — кипятит литр за 60 сек", "защита от детей блокировкой", "класс A — экономия энергии"], + cons: ["требует индукционной посуды (магнитное дно)", "нет FlexZone — нельзя объединить две зоны под крупную сковороду"], + reasoning: "Базовая индукция от лидера рынка 2026 РФ. Идеальна когда не нужны премиум-фичи.", tier: "middle", enriched: { - image_url: "https://placehold.co/200x200/F5EDDC/6B4A2B?text=Bosch+PUE", - rating_max: 4.6, reviews_total: 845, stores_count: 10, - wb: { url: "#" }, yamarket: { url: "#" } + image_url: "https://placehold.co/200x200/F5EDDC/6B4A2B?text=Haier+hob", + rating_max: 4.6, reviews_total: 845, stores_count: 4, + ozon: { url: "https://www.ozon.ru/product/haier-hhx-y64nfb/", price_min_rub: 39990 }, + citilink: { url: "https://www.citilink.ru/product/haier-hhx-y64nfb/", price_min_rub: 42500 }, + wb: { url: "#", price_min_rub: 44990 }, + yamarket: { url: "#", price_min_rub: 48000 } } }, { - brand: "Siemens", model: "EH675FJC1E", - price_min_rub: 79900, price_max_rub: 89000, - highlights: ["Индукция", "FlexZone (объединение зон)", "Hob2Hood (управление вытяжкой)"], - pros: ["варочная управляет вытяжкой", "5 зон + Flex", "8-летняя гарантия"], - cons: ["цена премиум-сегмента"], - tier: "premium", + brand: "Korting", model: "HI 64560 BB", + price_min_rub: 54990, price_max_rub: 62000, + highlights: ["Индукция", "FlexZone (объединение зон)", "9 уровней мощности"], + pros: ["FlexZone — объединяет 2 зоны для wok или большой кастрюли", "более чувствительный сенсор чем у Haier (9 vs 4 деления)", "5 лет официальной гарантии в РФ"], + cons: ["цена выше базового Haier на 38%", "управление сенсорное — иногда срабатывает по случайному касанию"], + reasoning: "Премиум-функционал FlexZone за разумные деньги. Для тех, кто часто готовит в wok.", + tier: "middle", enriched: { - image_url: "https://placehold.co/200x200/EFE3CC/6B4A2B?text=Siemens", - rating_max: 4.8, reviews_total: 432, stores_count: 6, - yamarket: { url: "#" }, ozon: { url: "#" } + image_url: "https://placehold.co/200x200/EFE3CC/6B4A2B?text=Korting", + rating_max: 4.8, reviews_total: 432, stores_count: 3, + ozon: { url: "https://www.ozon.ru/product/korting-hi64560bb/", price_min_rub: 54990 }, + citilink: { url: "https://www.citilink.ru/product/korting-hi64560bb/", price_min_rub: 58000 }, + yamarket: { url: "https://market.yandex.ru/product--korting-hi64560bb/", price_min_rub: 62000 } + } + }, + { + brand: "Bosch", model: "PUE611BB5E ⚠ПИ", + price_min_rub: 64990, price_max_rub: 78000, + highlights: ["Индукция", "PowerBoost", "PerfectCook (сенсор посуды)"], + pros: ["PerfectCook — сенсор определяет посуду и держит температуру", "лучший дизайн без рамки", "немецкая надёжность"], + cons: ["параллельный импорт — гарантия только продавца, не Bosch", "цена выше Haier на 65% за похожий функционал", "ожидание поставки 3-5 недель"], + reasoning: "Премиум-выбор для эстетов. Доступен только через ПИ, переплата за бренд значительная.", + tier: "middle", + enriched: { + image_url: "https://placehold.co/200x200/D8C9A8/6B4A2B?text=Bosch+%E2%9A%A0", + rating_max: 4.7, reviews_total: 1240, stores_count: 2, + ozon: { url: "https://www.ozon.ru/product/bosch-pue611bb5e/", price_min_rub: 64990 }, + yamarket: { url: "https://market.yandex.ru/product--bosch-pue611bb5e/", price_min_rub: 78000 } } } ] @@ -200,14 +229,14 @@ function renderReport(ai, leadId) { `); const modelsWrap = catNode.querySelector(".report-models"); + // Матрица цен — primary view, ВСТАВЛЯЕМ ДО списка моделей + const matrixNode = _renderPriceMatrix(models); + if (matrixNode) catNode.insertBefore(matrixNode, modelsWrap); + for (const m of models) { modelsWrap.appendChild(_renderModelCard(m)); } - if (models.length >= 2) { - modelsWrap.appendChild(_renderCompareTable(models)); - } - wrap.appendChild(catNode); } @@ -309,6 +338,80 @@ function _renderModelCard(m) { `); } +function _renderPriceMatrix(models) { + if (!models || !models.length) return null; + const STORES = [ + { key: "ozon", label: "OZON" }, + { key: "citilink", label: "Citilink" }, + { key: "wb", label: "Wildberries" }, + { key: "yamarket", label: "Я.Маркет" }, + { key: "dns", label: "DNS" }, + ]; + const activeStores = STORES.filter(s => + models.some(m => (m.enriched || {})[s.key] && ((m.enriched || {})[s.key].price_min_rub || (m.enriched || {})[s.key].url)) + ); + const showStoresCols = activeStores.length > 0; + + const head = ` + + + Модель + ${showStoresCols + ? activeStores.map(s => `${s.label}`).join("") + : `Цена (AI)`} + Мин + + + `; + + const rows = models.map(m => { + const enriched = m.enriched || {}; + const cellPrices = showStoresCols + ? activeStores.map(s => { + const item = enriched[s.key]; + return { store: s.key, price: item && item.price_min_rub, url: item && item.url }; + }) + : [{ store: "ai", price: enriched.price_min_rub || m.price_min_rub, url: null }]; + + const validPrices = cellPrices.map(c => c.price).filter(p => p && p > 0); + const bestPrice = validPrices.length ? Math.min(...validPrices) : null; + + const modelCell = ` + +
${_esc(m.brand || "")}
+
${_esc(m.model || "")}
+ + `; + const storeCells = cellPrices.map(c => { + if (!c.price) { + return c.url + ? `смотреть →` + : `—`; + } + const isBest = bestPrice && c.price === bestPrice; + const priceHtml = `${formatRub(c.price)} ₽`; + const cellInner = c.url + ? `${priceHtml}${isBest ? ' ' : ''}` + : `${priceHtml}${isBest ? ' ' : ''}`; + return `${cellInner}`; + }).join(""); + const bestCell = `${bestPrice ? `${formatRub(bestPrice)}` : "—"}`; + return `${modelCell}${storeCells}${bestCell}`; + }).join(""); + + return el(` +
+
Цены по магазинам — лучшая отмечена ✓
+
+ + ${head} + ${rows} +
+
+
+ `); +} + function _renderCompareTable(models) { const rows = models.map(m => { const e = m.enriched || {};