From 1908f841087962b78d2ccc9a3b6f54be857ea0fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Mon, 3 Aug 2026 23:40:12 +0300 Subject: [PATCH] =?UTF-8?q?fix(=D1=80=D0=B5=D0=BA=D0=BB=D0=B0=D0=BC=D0=B0)?= =?UTF-8?q?:=20=D1=81=D1=87=D1=91=D1=82=D1=87=D0=B8=D0=BA=20=D0=B0=D1=83?= =?UTF-8?q?=D0=B4=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D0=B8=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8?= =?UTF-8?q?=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20=D0=B8=20=D1=81=D0=BA?= =?UTF-8?q?=D0=BB=D0=B5=D0=B9=D0=BA=D0=B0=20=D0=BE=D0=B4=D0=B8=D0=BD=D0=B0?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2=D1=8B=D1=85=20=D1=81=D0=BB=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=AF=D0=BD=D0=B4=D0=B5=D0=BA=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Две мелочи из приёмки 03.08.2026, обе со сторожами, принятыми красным. 1. Счётчик аудитории врал после загрузки списка номеров. Загрузили 1693 номера — под кнопкой оставалось «Наберётся ~15 человек» и красное «аудитория меньше 100». Правда появлялась, только если тронуть любое другое поле. Клиент делает единственный разумный вывод — список не принялся — и грузит его снова. Починка: после удачной загрузки счётчик пересчитывается сам. Сторож держит не «позвали ли пересчёт», а то, что видит клиент на экране. 2. Лента кампании показывала стену из одинакового текста. Яндекс отвечает по каждому объявлению отдельно, и на общую беду «предоставьте документы» приходит пятнадцать одинаковых слов сразу. Замер на кампании #6: 15 сообщений, разных из них всего два — второе терялось в стене. Починка только на показе: одинаковый текст подряд склеивается в одну строку с подписью «и ещё N объявлений». Запись по объявлениям не тронута — по ней разбирают, кого именно не пустили. Второй сторож держит границу склейки: сообщения с приложенным файлом не склеиваются никогда, иначе ссылка на документ исчезнет с экрана. Проверено: 2045 сторожей фронта зелёные, типы Vue чисты, стиль в изменённых файлах чист. Бэкенд и схема не тронуты. Co-Authored-By: Claude Opus 5 (1M context) --- .../advertising/CampaignMessages.vue | 71 ++++++++++++++++++- .../components/advertising/CampaignWizard.vue | 4 ++ .../advertising-campaign-messages.spec.ts | 57 +++++++++++++++ .../advertising-campaign-wizard.spec.ts | 46 ++++++++++++ 4 files changed, 176 insertions(+), 2 deletions(-) diff --git a/app/resources/js/components/advertising/CampaignMessages.vue b/app/resources/js/components/advertising/CampaignMessages.vue index d3f183da..9651ba3a 100644 --- a/app/resources/js/components/advertising/CampaignMessages.vue +++ b/app/resources/js/components/advertising/CampaignMessages.vue @@ -3,7 +3,7 @@ * CampaignMessages — окно передачи между Яндексом и клиентом по одной кампании. * Слова Яндекса показываем как есть, без своей трактовки. */ -import { ref, watch } from 'vue'; +import { computed, ref, watch } from 'vue'; import { fetchCampaignMessages, sendCampaignMessage, type CampaignMessage } from '../../api/advertising'; const props = defineProps<{ campaignId: number | null }>(); @@ -55,6 +55,66 @@ async function send(): Promise { } } +/** + * Одинаковый текст подряд — одна строка на экране. + * + * Яндекс отвечает по каждому объявлению отдельно, и на общую беду («предоставьте + * документы») приходит пятнадцать одинаковых слов сразу. Записаны они по объявлениям + * не зря — по записи разбирают, кого именно не пустили, — но клиенту показывать стену + * из одинакового текста нельзя: в ней теряется второе, РАЗНОЕ сообщение. + * Замер 03.08.2026 на кампании #6: 15 сообщений, из них разных всего два. + * + * 🪤 Склеиваем только соседей: разнести одинаковые ответы, между которыми клиент писал + * своё, значило бы переставить переписку местами. И никогда не склеиваем сообщения + * с приложенным файлом — иначе ссылка на документ исчезнет с экрана. + */ +type SklejennoeSoobshchenie = { + id: number; + author: CampaignMessage['author']; + body: string; + file_name: string | null; + povtory: number; +}; + +const sklejennye = computed(() => { + const rows: SklejennoeSoobshchenie[] = []; + + for (const m of messages.value) { + const last = rows[rows.length - 1]; + const povtor = + last !== undefined && + last.file_name === null && + (m.file_name ?? null) === null && + last.author === m.author && + last.body === m.body; + + if (povtor && last !== undefined) { + last.povtory += 1; + continue; + } + + rows.push({ id: m.id, author: m.author, body: m.body, file_name: m.file_name ?? null, povtory: 0 }); + } + + return rows; +}); + +/** «и ещё 2 объявления» — с правильным русским окончанием. */ +function povtoryuSlovami(n: number): string { + const posledn = n % 10; + const desyatki = n % 100; + const slovo = + desyatki >= 11 && desyatki <= 14 + ? 'объявлений' + : posledn === 1 + ? 'объявление' + : posledn >= 2 && posledn <= 4 + ? 'объявления' + : 'объявлений'; + + return `и ещё ${n} ${slovo}`; +} + watch(() => props.campaignId, reload, { immediate: true }); @@ -65,13 +125,20 @@ watch(() => props.campaignId, reload, { immediate: true });

- + {{ AUTHOR_LABEL[m.author] }} {{ m.body }} + + {{ povtoryuSlovami(m.povtory) }} +