feat(реклама): поле «Цена за клик» в мастере кампании (T13) + чистка мёртвого блока
Шаг 3 «Бюджет» получил поле цены за клик (дефолт 10.00₽, как на бэке в CampaignLauncher), значение уходит в PATCH при сохранении бюджета и префиллится при открытии мастера на правку — иначе правка бюджета у существующей кампании тихо сбрасывала бы её цену за клик на дефолт. Сводка шага 4 показывает цену за клик и ссылку первого объявления. Убран мёртвый блок «Бюджет в день» — поле упразднено ранее, а условие на него всё ещё висело в шаблоне. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ export interface Campaign {
|
||||
*/
|
||||
use_uploaded_list?: boolean;
|
||||
daily_budget_rub?: string | null;
|
||||
click_bid_rub?: string | null;
|
||||
}
|
||||
|
||||
/** Ответ GET /api/advertising/campaigns/{id}/audience-size. */
|
||||
|
||||
@@ -72,6 +72,7 @@ const form = reactive<CampaignCreate & { ads: AdCreative[] }>({
|
||||
use_uploaded_list: false,
|
||||
weekly_budget_rub: '1000.00',
|
||||
daily_budget_rub: null,
|
||||
click_bid_rub: '10.00',
|
||||
ads: [],
|
||||
});
|
||||
|
||||
@@ -140,6 +141,7 @@ async function loadForEdit(id: number): Promise<void> {
|
||||
form.use_uploaded_list = detail.campaign.use_uploaded_list ?? false;
|
||||
form.weekly_budget_rub = detail.campaign.weekly_budget_rub;
|
||||
form.daily_budget_rub = detail.campaign.daily_budget_rub ?? null;
|
||||
form.click_bid_rub = detail.campaign.click_bid_rub ?? '10.00';
|
||||
form.ads = detail.ads ?? [];
|
||||
draftId.value = detail.campaign.id;
|
||||
await refreshAudienceSize();
|
||||
@@ -200,6 +202,7 @@ async function saveBudgetStep(): Promise<boolean> {
|
||||
weekly_budget_rub: form.weekly_budget_rub,
|
||||
audience_days: form.audience_days,
|
||||
use_uploaded_list: form.use_uploaded_list,
|
||||
click_bid_rub: form.click_bid_rub,
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -404,6 +407,19 @@ defineExpose({ form, draftId, audienceSize, canLaunch, refreshAudienceSize, budg
|
||||
data-testid="budget-weekly"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="form.click_bid_rub"
|
||||
type="number"
|
||||
label="Цена за клик"
|
||||
suffix="₽"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="mb-2"
|
||||
hint="Сколько максимум готовы платить за один переход"
|
||||
persistent-hint
|
||||
data-testid="budget-click-bid"
|
||||
/>
|
||||
|
||||
<p v-if="budgetError" class="text-body-2 text-error" data-testid="wizard-budget-error">
|
||||
{{ budgetError }}
|
||||
</p>
|
||||
@@ -430,9 +446,17 @@ defineExpose({ form, draftId, audienceSize, canLaunch, refreshAudienceSize, budg
|
||||
formatPlain(Number(form.weekly_budget_rub))
|
||||
}}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="form.daily_budget_rub" prepend-icon="mdi-calendar-today-outline">
|
||||
<v-list-item-title>Бюджет в день</v-list-item-title>
|
||||
<v-list-item-subtitle class="num">{{ form.daily_budget_rub }} ₽</v-list-item-subtitle>
|
||||
<v-list-item prepend-icon="mdi-cursor-default-click-outline">
|
||||
<v-list-item-title>Цена за клик</v-list-item-title>
|
||||
<v-list-item-subtitle class="num">{{
|
||||
formatPlain(Number(form.click_bid_rub))
|
||||
}}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="form.ads[0]?.href" prepend-icon="mdi-link-variant">
|
||||
<v-list-item-title>Сайт</v-list-item-title>
|
||||
<v-list-item-subtitle data-testid="launch-summary-site">{{
|
||||
form.ads[0]?.href
|
||||
}}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
|
||||
@@ -226,6 +226,44 @@ describe('CampaignWizard.vue (B2-7 — шаг 3 «Бюджет»)', () => {
|
||||
expect(note.exists()).toBe(true);
|
||||
expect(note.text()).toContain('340');
|
||||
});
|
||||
|
||||
it('T13 — на шаге 3 есть поле «Цена за клик» с дефолтом 10.00', async () => {
|
||||
createCampaignMock.mockResolvedValue(DRAFT_CAMPAIGN);
|
||||
fetchAudienceSizeMock.mockResolvedValue({ size: 120, min: 100, enough: true, hint: null });
|
||||
|
||||
const w = mount(CampaignWizard, { global: { plugins: [vuetify] } });
|
||||
await flush();
|
||||
await w.vm.$nextTick();
|
||||
|
||||
await gotoStep3(w);
|
||||
|
||||
const clickBidInput = w.find('[data-testid="budget-click-bid"] input');
|
||||
expect(clickBidInput.exists()).toBe(true);
|
||||
expect((clickBidInput.element as HTMLInputElement).value).toBe('10.00');
|
||||
});
|
||||
|
||||
it('T13 — правка цены за клик и «Далее» на шаге 3 уходит в patchCampaign click_bid_rub', async () => {
|
||||
createCampaignMock.mockResolvedValue(DRAFT_CAMPAIGN);
|
||||
fetchAudienceSizeMock.mockResolvedValue({ size: 120, min: 100, enough: true, hint: null });
|
||||
patchCampaignMock.mockResolvedValue({ ...DRAFT_CAMPAIGN, click_bid_rub: '15.00' });
|
||||
|
||||
const w = mount(CampaignWizard, { global: { plugins: [vuetify] } });
|
||||
await flush();
|
||||
await w.vm.$nextTick();
|
||||
|
||||
await gotoStep3(w);
|
||||
|
||||
const clickBidInput = w.find('[data-testid="budget-click-bid"] input');
|
||||
await clickBidInput.setValue('15');
|
||||
await w.vm.$nextTick();
|
||||
|
||||
await w.find('[data-testid="wizard-next"]').trigger('click');
|
||||
await flush();
|
||||
await w.vm.$nextTick();
|
||||
|
||||
expect(patchCampaignMock).toHaveBeenCalledWith(99, expect.objectContaining({ click_bid_rub: '15' }));
|
||||
expect(w.find('[data-testid="wizard-step-4"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CampaignWizard.vue (B2-8 — шаг 4 «Проверка и запуск»)', () => {
|
||||
@@ -330,6 +368,22 @@ describe('CampaignWizard.vue (B2-8 — шаг 4 «Проверка и запус
|
||||
expect(errorEl.text()).toContain('Аудитория слишком мала для запуска.');
|
||||
expect(w.emitted('launched')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('T13 — сводка шага 4 показывает цену за клик', async () => {
|
||||
createCampaignMock.mockResolvedValue(DRAFT_CAMPAIGN);
|
||||
fetchAudienceSizeMock.mockResolvedValue({ size: 120, min: 100, enough: true, hint: null });
|
||||
patchCampaignMock.mockResolvedValue({ ...DRAFT_CAMPAIGN, weekly_budget_rub: '1000.00' });
|
||||
|
||||
const w = mount(CampaignWizard, { global: { plugins: [vuetify] } });
|
||||
await flush();
|
||||
await w.vm.$nextTick();
|
||||
|
||||
await gotoStep4(w);
|
||||
|
||||
const summary = w.find('[data-testid="launch-summary"]');
|
||||
expect(summary.text()).toContain('Цена за клик');
|
||||
expect(summary.text()).toContain('10');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CampaignWizard.vue (B2-9 — правка на ходу, Р30)', () => {
|
||||
|
||||
Reference in New Issue
Block a user