561096cc84
Претензия владельца 12.07.2026: «видео мало и они обрубленные». Причина «обрубленности» найдена живым прогоном: раннер НЕ переключал экраны — шаг про «Проекты» показывался поверх «Биллинга». Фикс GuidedTour: при переходе на шаг с другим route зовём router.push (TDD-тест «шаг на другом экране»). Каталог: 6 → 15 экскурсий, все минимум по 3 шага (тест-сторож в TourCatalog.spec): start (4 шага), create-project (6), top-up-balance (6), tariffs (4), balance-pause (4), deals (4), kanban (3), konkurentnoe-pole (6), change-source (4), security (4), notifications (3), import (4), api (3), dashboard (4), support (3). Якоря data-tour расставлены по портале (3 агента): биллинг (кошелёк, запас, ступени, вкладки, сумма, способ оплаты), проекты (фильтры, карточка, меню, тип источника, лимит, регионы, дни), сделки (фильтры, таблица, экспорт), канбан, импорт, дашборд, настройки (вкладки, 2FA, матрица уведомлений), Поле (собрать, вручную, дубли, вкладки, карточка, легенда, изучение), помощь (FAQ, форма). Каждая из 17 статей инструкции получила свою экскурсию (frontmatter tour:). Vitest 1216/1216, Pest бот 46/46, живая проверка в браузере: экскурсия сама переходит между экранами и подсвечивает цели. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
209 lines
8.5 KiB
Vue
209 lines
8.5 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* BalanceCard — верхний ряд биллинга: Кошелёк ₽ (тёмный герой) → ≈ Лиды → Запас.
|
||
* Данные — из GET /api/billing/wallet (E3) + живой заказ из tenant-store.
|
||
*
|
||
* 22.06.2026: карточка «Тариф» убрана — тарифов как сущности нет (цена за лид по
|
||
* объёму, 7 ступеней). На её место — «Запас»: на сколько хватит баланса ПРИ ТЕКУЩЕМ
|
||
* заказе проектов (requiredLeadsPerDay). Считаем так же, как BalanceCapacityIndicator,
|
||
* чтобы число согласовалось с подписью «при N лидов в день»: 0/день → ∞ (не расходуется),
|
||
* N/день → affordableLeads / N. НЕ берём бэкендный runway_days (он по исторической
|
||
* скорости за 30 дней и противоречит живому заказу).
|
||
*/
|
||
import { computed } from 'vue';
|
||
import { leadsWord } from '../../utils/plural';
|
||
|
||
const props = defineProps<{
|
||
walletRub: number;
|
||
affordableLeads: number;
|
||
currentTierPriceRub: string;
|
||
requiredLeadsPerDay?: number;
|
||
}>();
|
||
|
||
defineEmits<{ topup: [] }>();
|
||
|
||
const walletText = computed(() => new Intl.NumberFormat('ru-RU').format(props.walletRub));
|
||
const affordableLeadsText = computed(() => new Intl.NumberFormat('ru-RU').format(props.affordableLeads));
|
||
|
||
// Живой заказ проектов (лидов/день). 0 → нет активных проектов → прогноз не считаем.
|
||
// B1-2 (UX-аудит 25.06): раньше показывали «∞ дн.», а дашборд «0 дней» — разнобой.
|
||
// Унифицировано: оба показывают «—» + «нет активных проектов».
|
||
const perDay = computed(() => Number(props.requiredLeadsPerDay ?? 0));
|
||
const runwayDays = computed(() => (perDay.value > 0 ? Math.floor(props.affordableLeads / perDay.value) : null));
|
||
const runwayText = computed(() => (runwayDays.value === null ? '—' : `≈ ${runwayDays.value} дн.`));
|
||
const runwaySub = computed(() =>
|
||
perDay.value > 0 ? `при ${perDay.value} ${leadsWord(perDay.value)} в день` : 'нет активных проектов',
|
||
);
|
||
</script>
|
||
|
||
<template>
|
||
<div class="chain-cards mt-4">
|
||
<!-- Кошелёк ₽ — тёмный герой, главное действие «Пополнить». -->
|
||
<v-card variant="flat" color="secondary" class="wallet-card primary pa-4" data-tour="billing-wallet">
|
||
<div class="wallet-h">
|
||
<span class="wallet-label">Кошелёк ₽</span>
|
||
<v-chip size="x-small" color="primary" variant="elevated">LIVE</v-chip>
|
||
</div>
|
||
<div class="wallet-amount mt-2">
|
||
<span class="num">{{ walletText }}</span>
|
||
<span class="ru"> ₽</span>
|
||
</div>
|
||
<div class="wallet-foot mt-3">мин. пополнение <strong>100 ₽</strong></div>
|
||
<div class="wallet-actions mt-3">
|
||
<v-btn color="primary" variant="flat" prepend-icon="mdi-plus" size="small" @click="$emit('topup')"
|
||
>Пополнить</v-btn
|
||
>
|
||
<v-tooltip text="Автопополнение будет доступно после подключения платёжного шлюза.">
|
||
<template #activator="{ props: tipProps }">
|
||
<span v-bind="tipProps" class="d-inline-flex">
|
||
<v-btn variant="outlined" prepend-icon="mdi-autorenew" size="small" disabled>
|
||
Автопополнение
|
||
</v-btn>
|
||
</span>
|
||
</template>
|
||
</v-tooltip>
|
||
</div>
|
||
</v-card>
|
||
|
||
<div class="chain-arrow" aria-hidden="true">→</div>
|
||
|
||
<!-- ≈ Лиды — сколько лидов покрывает баланс по текущей цене. -->
|
||
<v-card variant="flat" class="wallet-card pa-4">
|
||
<span class="wallet-label">≈ Лиды</span>
|
||
<div class="wallet-amount mt-2">
|
||
<span class="num">≈ {{ affordableLeadsText }}</span>
|
||
<span class="ru-text"> {{ leadsWord(affordableLeads) }}</span>
|
||
</div>
|
||
<div class="wallet-foot mt-2">
|
||
сейчас по {{ currentTierPriceRub }} ₽/лид
|
||
<v-tooltip text="Точный расчёт по текущим ценам. Меняется при переходе ступеней.">
|
||
<template #activator="{ props: tipProps }">
|
||
<v-icon v-bind="tipProps" size="14" class="ml-1">mdi-information-outline</v-icon>
|
||
</template>
|
||
</v-tooltip>
|
||
</div>
|
||
</v-card>
|
||
|
||
<div class="chain-arrow" aria-hidden="true">→</div>
|
||
|
||
<!-- Запас — на сколько хватит баланса при текущем заказе проектов. -->
|
||
<v-card variant="flat" class="wallet-card pa-4" data-tour="billing-runway">
|
||
<span class="wallet-label">Запас</span>
|
||
<div class="wallet-amount mt-2">
|
||
<span class="num">{{ runwayText }}</span>
|
||
</div>
|
||
<div class="wallet-foot mt-2">
|
||
{{ runwaySub }}
|
||
<v-tooltip text="На сколько хватит баланса при текущем дневном заказе проектов. Без заказов баланс не расходуется.">
|
||
<template #activator="{ props: tipProps }">
|
||
<v-icon v-bind="tipProps" size="14" class="ml-1">mdi-information-outline</v-icon>
|
||
</template>
|
||
</v-tooltip>
|
||
</div>
|
||
</v-card>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.num {
|
||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||
font-feature-settings: 'tnum';
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* Ряд из трёх карточек одинаковой высоты + стрелки между ними. */
|
||
.chain-cards {
|
||
display: flex;
|
||
align-items: stretch;
|
||
gap: 0;
|
||
}
|
||
.chain-cards > .wallet-card {
|
||
flex: 1 1 0;
|
||
/* height НЕ задаём — align-items: stretch уже растягивает все три до одной высоты */
|
||
}
|
||
.chain-arrow {
|
||
flex: 0 0 auto;
|
||
align-self: center;
|
||
padding: 0 12px;
|
||
color: #b6a98c;
|
||
font-size: 22px;
|
||
line-height: 1;
|
||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||
user-select: none;
|
||
}
|
||
|
||
/* На узких экранах ряд складывается в столбец, стрелки поворачиваются вниз. */
|
||
@media (max-width: 960px) {
|
||
.chain-cards {
|
||
flex-direction: column;
|
||
}
|
||
/* В столбце плитки — по высоте контента (иначе flex-basis:0 схлопывал их,
|
||
а overflow:hidden обрезал цифры/кнопки — «Кошелёк/Лиды/Запас» пустели). */
|
||
.chain-cards > .wallet-card {
|
||
flex: 0 0 auto;
|
||
}
|
||
.chain-arrow {
|
||
transform: rotate(90deg);
|
||
padding: 8px 0;
|
||
}
|
||
}
|
||
|
||
/* Единый «фрейм» всех трёх карточек: одинаковая рамка, радиус, фон. */
|
||
.wallet-card {
|
||
border: 1px solid #e4e0d7;
|
||
border-radius: 14px;
|
||
background: #fff;
|
||
}
|
||
.wallet-card.primary {
|
||
color: #fff;
|
||
background: #012019 !important;
|
||
border-color: #012019;
|
||
}
|
||
.wallet-h {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
.wallet-label {
|
||
font-size: 12px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
color: #66635c;
|
||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||
}
|
||
.wallet-card.primary .wallet-label {
|
||
color: #7a8c87;
|
||
}
|
||
.wallet-amount {
|
||
font-size: 32px;
|
||
font-weight: 600;
|
||
line-height: 1.1;
|
||
}
|
||
.wallet-amount .num {
|
||
color: inherit;
|
||
letter-spacing: -0.01em;
|
||
}
|
||
.wallet-amount .ru,
|
||
.wallet-amount .ru-text {
|
||
color: #66635c;
|
||
font-weight: 500;
|
||
font-size: 18px;
|
||
}
|
||
.wallet-card.primary .wallet-amount .ru {
|
||
color: #7a8c87;
|
||
}
|
||
.wallet-foot {
|
||
color: inherit;
|
||
opacity: 0.7;
|
||
font-size: 12px;
|
||
}
|
||
.wallet-card.primary .wallet-foot {
|
||
color: #b1c2bd;
|
||
opacity: 1;
|
||
}
|
||
.wallet-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
</style>
|