Files
portal/app/resources/js/components/billing/BalanceCard.vue
T
Дмитрий 849bc73290 refactor(frontend): Sprint 4 Phase B/2 — split 3 user views (audit O-refactor-04 хвост)
BillingView 416→114 (+ BalanceCard 155 + TransactionsTable 113 + InvoicesTable 90
+ billingFormatters 51 composable: formatPlain/formatCost/statusChipColor/
statusLabel/formatLabel/formatIcon/txAmountClass).
SecurityTab 354→39 (+ ChangePasswordCard 17 + TwoFactorCard 218 + RecoveryCodesCard 104
+ SessionsTable 66; auth-store читается напрямую в каждом sub-component).
RemindersView 345→183 (+ RemindersFilters 51 + RemindersList 173;
ReminderDialog уже отдельный с прошлой фазы — служит как ReminderForm).

State (`activeTab`, `editingReminder`, `deletingReminderId` в RemindersView)
остаётся в parent ради единого reload-flow + confirm-dialog'ов. Auth-store
читается напрямую в TwoFactorCard/RecoveryCodesCard через useAuthStore() —
без prop-drilling. Reminders-store читается напрямую в RemindersFilters/
RemindersList.

Все sub-components <250 строк (acceptance threshold). 3 view-shells: 114/39/183.

Регрессия: ESLint 0 + vue-tsc 0 + Vitest 416/416 + build OK 968 ms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 04:46:14 +03:00

156 lines
4.5 KiB
Vue

<script setup lang="ts">
/**
* BalanceCard — 3 wallet-cards в одной строке: Кошелёк ₽ (primary, dark) +
* Баланс лидов + Тариф. Sprint 4 Phase B/2 — split BillingView (audit O-refactor-04 хвост).
*/
defineProps<{
walletRub: number;
leadsBalance: number;
tariffName: string;
tariffPrice: number;
tariffFeatures: string[];
}>();
</script>
<template>
<v-row dense class="wallet-row mt-4">
<v-col cols="12" md="4">
<v-card variant="flat" color="secondary" class="wallet-card primary pa-4">
<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">{{ new Intl.NumberFormat('ru-RU').format(walletRub) }}</span>
<span class="ru">&nbsp;</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">Пополнить</v-btn>
<v-btn variant="outlined" prepend-icon="mdi-autorenew" size="small"> Автопополнение </v-btn>
</div>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card variant="outlined" class="wallet-card pa-4">
<div class="wallet-h">
<span class="wallet-label">Баланс лидов (ГЦК)</span>
</div>
<div class="wallet-amount mt-2">
<span class="num">{{ leadsBalance }}</span>
<span class="ru-text">&nbsp;лидов</span>
</div>
<div class="wallet-foot mt-3">средняя цена <strong>50 /лид</strong> · потрачено за месяц 412</div>
</v-card>
</v-col>
<v-col cols="12" md="4">
<v-card variant="outlined" class="wallet-card pa-4 d-flex flex-column">
<span class="wallet-label">Тариф</span>
<div class="tariff-name mt-1">
{{ tariffName }}
<span class="tariff-price">· {{ tariffPrice }} /мес</span>
</div>
<ul class="tariff-feats mt-3">
<li v-for="f in tariffFeatures" :key="f">
<v-icon size="14" color="success" class="mr-1">mdi-check</v-icon>{{ f }}
</li>
</ul>
<v-btn variant="outlined" size="small" class="mt-auto">Сменить тариф </v-btn>
</v-card>
</v-col>
</v-row>
</template>
<style scoped>
.num {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-feature-settings: 'tnum';
font-weight: 500;
}
.wallet-card {
height: 100%;
background: #fff;
}
.wallet-card.primary {
color: #fff;
background: #012019 !important;
}
.wallet-h {
display: flex;
align-items: center;
justify-content: space-between;
}
.wallet-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #7a8c87;
font-family: 'JetBrains Mono', ui-monospace, monospace;
}
.wallet-card:not(.primary) .wallet-label {
color: #66635c;
}
.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;
}
.tariff-name {
font-weight: 600;
font-size: 17px;
color: #081319;
}
.tariff-price {
font-family: 'JetBrains Mono', ui-monospace, monospace;
color: #0f6e56;
font-size: 13px;
font-weight: 500;
margin-left: 4px;
}
.tariff-feats {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 6px;
flex: 1;
}
.tariff-feats li {
font-size: 13px;
color: #343c41;
display: flex;
align-items: center;
}
</style>