mirror of
https://github.com/wasrusgen/zov-tech.git
synced 2026-06-03 15:44:47 +00:00
miniapp: price comparison matrix as PRIMARY view per category
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
This commit is contained in:
parent
ca342c0641
commit
d7be644aed
@ -1392,7 +1392,149 @@
|
|||||||
}
|
}
|
||||||
.report-link:active { background: var(--warm); }
|
.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 {
|
.report-compare {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
|
|||||||
@ -1265,19 +1265,19 @@ const Podbor = (function () {
|
|||||||
${_esc(catLabel)}
|
${_esc(catLabel)}
|
||||||
</h3>
|
</h3>
|
||||||
${catAnalysis ? `<div class="report-cat-analysis">${_esc(catAnalysis)}</div>` : ""}
|
${catAnalysis ? `<div class="report-cat-analysis">${_esc(catAnalysis)}</div>` : ""}
|
||||||
<div class="report-models"></div>
|
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
const modelsWrap = catNode.querySelector(".report-models");
|
|
||||||
|
|
||||||
|
// Сравнение цен — основной блок, всегда вверху
|
||||||
|
const matrixNode = _renderPriceMatrix(models);
|
||||||
|
if (matrixNode) catNode.appendChild(matrixNode);
|
||||||
|
|
||||||
|
// Детальные карточки с pros/cons
|
||||||
|
const modelsWrap = el(`<div class="report-models"></div>`);
|
||||||
for (const m of models) {
|
for (const m of models) {
|
||||||
modelsWrap.appendChild(_renderModelCard(m));
|
modelsWrap.appendChild(_renderModelCard(m));
|
||||||
}
|
}
|
||||||
|
catNode.appendChild(modelsWrap);
|
||||||
// Сравнение
|
|
||||||
if (models.length >= 2) {
|
|
||||||
modelsWrap.appendChild(_renderCompareTable(models));
|
|
||||||
}
|
|
||||||
|
|
||||||
wrap.appendChild(catNode);
|
wrap.appendChild(catNode);
|
||||||
}
|
}
|
||||||
@ -1384,6 +1384,93 @@ const Podbor = (function () {
|
|||||||
return "магазинов";
|
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 = `
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="col-model">Модель</th>
|
||||||
|
${showStoresCols
|
||||||
|
? activeStores.map(s => `<th class="col-store">${s.label}</th>`).join("")
|
||||||
|
: `<th class="col-store">Цена (AI)</th>`}
|
||||||
|
<th class="col-best">Мин</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<td class="col-model">
|
||||||
|
<div class="m-brand">${_esc(m.brand || "")}</div>
|
||||||
|
<div class="m-name">${_esc(m.model || "")}</div>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
const storeCells = cellPrices.map(c => {
|
||||||
|
if (!c.price) {
|
||||||
|
return c.url
|
||||||
|
? `<td class="cell-price"><a href="${_esc(c.url)}" target="_blank" rel="noopener noreferrer" class="cell-noprice-link">смотреть →</a></td>`
|
||||||
|
: `<td class="cell-price empty">—</td>`;
|
||||||
|
}
|
||||||
|
const isBest = bestPrice && c.price === bestPrice;
|
||||||
|
const priceHtml = `<strong>${formatRub(c.price)}</strong> ₽`;
|
||||||
|
const cellInner = c.url
|
||||||
|
? `<a href="${_esc(c.url)}" target="_blank" rel="noopener noreferrer">${priceHtml}${isBest ? ' <span class="best-mark">✓</span>' : ''}</a>`
|
||||||
|
: `${priceHtml}${isBest ? ' <span class="best-mark">✓</span>' : ''}`;
|
||||||
|
return `<td class="cell-price${isBest ? ' best' : ''}">${cellInner}</td>`;
|
||||||
|
}).join("");
|
||||||
|
const bestCell = `<td class="col-best">${bestPrice ? `<strong>${formatRub(bestPrice)}</strong>` : "—"}</td>`;
|
||||||
|
|
||||||
|
return `<tr>${modelCell}${storeCells}${bestCell}</tr>`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
return el(`
|
||||||
|
<div class="report-matrix-wrap">
|
||||||
|
<div class="report-matrix-head">Цены по магазинам — лучшая отмечена ✓</div>
|
||||||
|
<div class="report-matrix-scroll">
|
||||||
|
<table class="report-matrix">
|
||||||
|
${head}
|
||||||
|
<tbody>${rows}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
${!showStoresCols ? `<div class="report-matrix-hint">Магазины ещё не нашли товар. Цены — оценка AI на основе текущих рыночных данных.</div>` : ""}
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
function _renderCompareTable(models) {
|
function _renderCompareTable(models) {
|
||||||
const rows = models.map(m => {
|
const rows = models.map(m => {
|
||||||
const e = m.enriched || {};
|
const e = m.enriched || {};
|
||||||
|
|||||||
@ -12,8 +12,8 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&display=swap">
|
||||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
||||||
<link rel="stylesheet" href="assets/styles.css?v=20260511h">
|
<link rel="stylesheet" href="assets/styles.css?v=20260511i">
|
||||||
<link rel="stylesheet" href="assets/podbor.css?v=20260511h">
|
<link rel="stylesheet" href="assets/podbor.css?v=20260511i">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main id="app">
|
<main id="app">
|
||||||
@ -21,10 +21,10 @@
|
|||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script src="assets/icons.js?v=20260511h"></script>
|
<script src="assets/icons.js?v=20260511i"></script>
|
||||||
<script src="assets/podbor.config.js?v=20260511h"></script>
|
<script src="assets/podbor.config.js?v=20260511i"></script>
|
||||||
<script src="assets/podbor.picts.js?v=20260511h"></script>
|
<script src="assets/podbor.picts.js?v=20260511i"></script>
|
||||||
<script src="assets/podbor.js?v=20260511h"></script>
|
<script src="assets/podbor.js?v=20260511i"></script>
|
||||||
<script src="assets/app.js?v=20260511h"></script>
|
<script src="assets/app.js?v=20260511i"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -76,60 +76,89 @@ const MOCK_AI = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
brand: "Liebherr", model: "CNd 5223",
|
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"],
|
highlights: ["BioFresh (зона свежести)", "NoFrost", "SoftClose"],
|
||||||
pros: ["премиум-качество сборки", "очень тихий 34дБ", "10 лет гарантии на компрессор"],
|
pros: ["премиум-качество немецкой сборки", "очень тихий 34 дБ — на 2 дБ тише Haier", "BioFresh — овощи дольше хрустящие на 30 дней", "10 лет гарантии на компрессор"],
|
||||||
cons: ["цена выше среднего на +15%"],
|
cons: ["цена выше Haier на ~50% при том же объёме", "идёт параллельным импортом — ждать 4-6 недель"],
|
||||||
|
reasoning: "Премиум-выбор для тех, кому важна зона свежести и тишина. Переплата ~50% за бренд и BioFresh.",
|
||||||
tier: "premium",
|
tier: "premium",
|
||||||
enriched: {
|
enriched: {
|
||||||
image_url: "https://placehold.co/200x200/EFE3CC/6B4A2B?text=Liebherr",
|
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: 4,
|
||||||
rating_max: 4.9, reviews_total: 873, stores_count: 8,
|
ozon: { url: "https://www.ozon.ru/product/liebherr-cnd5223/", price_min_rub: 134900 },
|
||||||
wb: { url: "#" }, yamarket: { url: "#" }
|
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,
|
price_min_rub: 39990,
|
||||||
highlights: ["LowFrost (размораживать только мороз)", "класс A+"],
|
highlights: ["LowFrost (размораживать только мороз)", "класс A+"],
|
||||||
pros: ["доступная цена", "компактные габариты"],
|
pros: ["доступная цена в 2 раза ниже Haier", "компактные габариты 600×600×1900 мм — идеальная ниша", "официальная гарантия 3 года от российского производителя"],
|
||||||
cons: ["без инвертора", "шум 42дБ"],
|
cons: ["без инвертора — компрессор работает циклами, заметнее на слух", "шум 42 дБ — на 6 дБ громче Haier", "нет зон свежести"],
|
||||||
|
reasoning: "Страховочный бюджет-вариант если клиент не хочет переплачивать. Российский, доступный, надёжный.",
|
||||||
tier: "budget",
|
tier: "budget",
|
||||||
enriched: {
|
enriched: {
|
||||||
image_url: "https://placehold.co/200x200/FBF7F0/6B4A2B?text=Indesit",
|
image_url: "https://placehold.co/200x200/FBF7F0/6B4A2B?text=Birusa",
|
||||||
price_min_rub: 39990, price_max_rub: 44900,
|
rating_max: 4.3, reviews_total: 2100, stores_count: 5,
|
||||||
rating_max: 4.3, reviews_total: 2100, stores_count: 15,
|
ozon: { url: "https://www.ozon.ru/product/biryusa-m124/", price_min_rub: 39990 },
|
||||||
dns: { url: "#" }, ozon: { url: "#" }
|
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: {
|
hob: {
|
||||||
|
analysis: "В 2026 году индукционные варочные среднего сегмента — это Haier и Korting. Bosch ещё доступен через ПИ, но цена выше на 25%. Все три модели — 60 см, 4 зоны, индукция.",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
brand: "Bosch", model: "PUE611BB5E",
|
brand: "Haier", label: "Haier",
|
||||||
price_min_rub: 59900, price_max_rub: 68000,
|
model: "HHX-Y64NFB",
|
||||||
highlights: ["Индукция", "PowerBoost (форсаж — кипятит за минуту)", "TouchSelect (плавная регулировка)"],
|
price_min_rub: 39990, price_max_rub: 48000,
|
||||||
pros: ["4 индукционные зоны", "безопасность для детей", "класс A"],
|
highlights: ["Индукция (магнитный нагрев посуды)", "Booster (кипятит за минуту)", "Сенсорное управление"],
|
||||||
cons: ["требует индукционной посуды"],
|
pros: ["4 индукционные зоны 60 см — стандарт", "Booster на 2 зонах — кипятит литр за 60 сек", "защита от детей блокировкой", "класс A — экономия энергии"],
|
||||||
|
cons: ["требует индукционной посуды (магнитное дно)", "нет FlexZone — нельзя объединить две зоны под крупную сковороду"],
|
||||||
|
reasoning: "Базовая индукция от лидера рынка 2026 РФ. Идеальна когда не нужны премиум-фичи.",
|
||||||
tier: "middle",
|
tier: "middle",
|
||||||
enriched: {
|
enriched: {
|
||||||
image_url: "https://placehold.co/200x200/F5EDDC/6B4A2B?text=Bosch+PUE",
|
image_url: "https://placehold.co/200x200/F5EDDC/6B4A2B?text=Haier+hob",
|
||||||
rating_max: 4.6, reviews_total: 845, stores_count: 10,
|
rating_max: 4.6, reviews_total: 845, stores_count: 4,
|
||||||
wb: { url: "#" }, yamarket: { url: "#" }
|
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",
|
brand: "Korting", model: "HI 64560 BB",
|
||||||
price_min_rub: 79900, price_max_rub: 89000,
|
price_min_rub: 54990, price_max_rub: 62000,
|
||||||
highlights: ["Индукция", "FlexZone (объединение зон)", "Hob2Hood (управление вытяжкой)"],
|
highlights: ["Индукция", "FlexZone (объединение зон)", "9 уровней мощности"],
|
||||||
pros: ["варочная управляет вытяжкой", "5 зон + Flex", "8-летняя гарантия"],
|
pros: ["FlexZone — объединяет 2 зоны для wok или большой кастрюли", "более чувствительный сенсор чем у Haier (9 vs 4 деления)", "5 лет официальной гарантии в РФ"],
|
||||||
cons: ["цена премиум-сегмента"],
|
cons: ["цена выше базового Haier на 38%", "управление сенсорное — иногда срабатывает по случайному касанию"],
|
||||||
tier: "premium",
|
reasoning: "Премиум-функционал FlexZone за разумные деньги. Для тех, кто часто готовит в wok.",
|
||||||
|
tier: "middle",
|
||||||
enriched: {
|
enriched: {
|
||||||
image_url: "https://placehold.co/200x200/EFE3CC/6B4A2B?text=Siemens",
|
image_url: "https://placehold.co/200x200/EFE3CC/6B4A2B?text=Korting",
|
||||||
rating_max: 4.8, reviews_total: 432, stores_count: 6,
|
rating_max: 4.8, reviews_total: 432, stores_count: 3,
|
||||||
yamarket: { url: "#" }, ozon: { url: "#" }
|
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");
|
const modelsWrap = catNode.querySelector(".report-models");
|
||||||
|
|
||||||
|
// Матрица цен — primary view, ВСТАВЛЯЕМ ДО списка моделей
|
||||||
|
const matrixNode = _renderPriceMatrix(models);
|
||||||
|
if (matrixNode) catNode.insertBefore(matrixNode, modelsWrap);
|
||||||
|
|
||||||
for (const m of models) {
|
for (const m of models) {
|
||||||
modelsWrap.appendChild(_renderModelCard(m));
|
modelsWrap.appendChild(_renderModelCard(m));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (models.length >= 2) {
|
|
||||||
modelsWrap.appendChild(_renderCompareTable(models));
|
|
||||||
}
|
|
||||||
|
|
||||||
wrap.appendChild(catNode);
|
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 = `
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="col-model">Модель</th>
|
||||||
|
${showStoresCols
|
||||||
|
? activeStores.map(s => `<th class="col-store">${s.label}</th>`).join("")
|
||||||
|
: `<th class="col-store">Цена (AI)</th>`}
|
||||||
|
<th class="col-best">Мин</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 = `
|
||||||
|
<td class="col-model">
|
||||||
|
<div class="m-brand">${_esc(m.brand || "")}</div>
|
||||||
|
<div class="m-name">${_esc(m.model || "")}</div>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
const storeCells = cellPrices.map(c => {
|
||||||
|
if (!c.price) {
|
||||||
|
return c.url
|
||||||
|
? `<td class="cell-price"><a href="${_esc(c.url)}" target="_blank" rel="noopener noreferrer" class="cell-noprice-link">смотреть →</a></td>`
|
||||||
|
: `<td class="cell-price empty">—</td>`;
|
||||||
|
}
|
||||||
|
const isBest = bestPrice && c.price === bestPrice;
|
||||||
|
const priceHtml = `<strong>${formatRub(c.price)}</strong> ₽`;
|
||||||
|
const cellInner = c.url
|
||||||
|
? `<a href="${_esc(c.url)}" target="_blank" rel="noopener noreferrer">${priceHtml}${isBest ? ' <span class="best-mark">✓</span>' : ''}</a>`
|
||||||
|
: `${priceHtml}${isBest ? ' <span class="best-mark">✓</span>' : ''}`;
|
||||||
|
return `<td class="cell-price${isBest ? ' best' : ''}">${cellInner}</td>`;
|
||||||
|
}).join("");
|
||||||
|
const bestCell = `<td class="col-best">${bestPrice ? `<strong>${formatRub(bestPrice)}</strong>` : "—"}</td>`;
|
||||||
|
return `<tr>${modelCell}${storeCells}${bestCell}</tr>`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
return el(`
|
||||||
|
<div class="report-matrix-wrap">
|
||||||
|
<div class="report-matrix-head">Цены по магазинам — лучшая отмечена ✓</div>
|
||||||
|
<div class="report-matrix-scroll">
|
||||||
|
<table class="report-matrix">
|
||||||
|
${head}
|
||||||
|
<tbody>${rows}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
function _renderCompareTable(models) {
|
function _renderCompareTable(models) {
|
||||||
const rows = models.map(m => {
|
const rows = models.map(m => {
|
||||||
const e = m.enriched || {};
|
const e = m.enriched || {};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user