feat(портал-мобилка): «Канбан» на телефоне — одна колонка + переключатель этапов

На телефоне 5 колонок больше не листаются вбок: сверху горизонтальный
переключатель этапов (чипсы со счётчиками), ниже — выбранная колонка на всю
ширину. Смена этапа сделки на телефоне — тапом по карточке через drawer
(drag вбок на узком экране неудобен). Планшет/десктоп — прежняя доска со всеми
колонками и перетаскиванием, без изменений.

Тесты Канбана 23/23 зелёные. Проверено на 390 и 1440px (+ переключение этапов).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-07-07 19:35:58 +03:00
parent c400dcbc4b
commit 9808bd00af
+106 -2
View File
@@ -32,11 +32,18 @@ import DealDetailDrawer from '../components/deals/DealDetailDrawer.vue';
import NewDealDialog from '../components/deals/NewDealDialog.vue';
import { useAuthStore } from '../stores/auth';
import { useLeadStatusesStore } from '../stores/leadStatuses';
import { useLayoutTier } from '../composables/useLayoutTier';
const auth = useAuthStore();
const leadStatusesStore = useLeadStatusesStore();
const leadStatuses = computed(() => leadStatusesStore.statuses);
// Телефон: одна колонка + переключатель этапов (drag вбок неудобен на узком экране;
// смена этапа сделки — тапом по карточке через drawer).
const { isPhone } = useLayoutTier();
const activeSlug = ref<string>(LEAD_STATUSES[0]?.slug ?? '');
const activeStatus = computed(() => leadStatuses.value.find((s) => s.slug === activeSlug.value) ?? leadStatuses.value[0] ?? null);
interface DraggableChangeEvent {
added?: { element: MockDeal; newIndex: number };
removed?: { element: MockDeal; oldIndex: number };
@@ -222,7 +229,9 @@ defineExpose({
><span class="num">{{ totalDeals }}</span> сделок</span
>
<span class="sep">·</span>
<span class="text-caption text-medium-emphasis"> Перетаскивание между колонками </span>
<span class="text-caption text-medium-emphasis">{{
isPhone ? 'Выберите этап' : 'Перетаскивание между колонками'
}}</span>
</div>
</div>
<div class="d-flex ga-2">
@@ -244,7 +253,37 @@ defineExpose({
Не удалось загрузить сделки. Попробуйте обновить.
</v-alert>
<div class="kanban-board mt-4" tabindex="0" role="region" aria-label="Канбан-доска воронки продаж">
<!-- Телефон: переключатель этапов + одна колонка на всю ширину -->
<div v-if="isPhone" class="kanban-mobile mt-4">
<div class="stage-switcher" role="tablist" aria-label="Этапы воронки">
<button
v-for="status in leadStatuses"
:key="status.slug"
type="button"
role="tab"
:aria-selected="status.slug === activeSlug"
class="stage-chip"
:class="{ 'stage-chip--active': status.slug === activeSlug }"
:style="{ '--accent': status.colorHex }"
@click="activeSlug = status.slug"
>
<span class="stage-chip__name">{{ status.nameRu }}</span>
<span class="stage-chip__count">{{ (dealsByStatus[status.slug] || []).length }}</span>
</button>
</div>
<KanbanColumn
v-if="activeStatus"
:key="activeStatus.slug"
:status="activeStatus"
:deals="dealsByStatus[activeSlug] || []"
@update:deals="(list) => (dealsByStatus[activeSlug] = list)"
@change="(e) => onColumnChange(activeSlug as MockDeal['statusSlug'], e)"
@open-deal="onOpenDeal"
/>
</div>
<!-- Планшет/десктоп: все колонки с горизонтальной прокруткой -->
<div v-else class="kanban-board mt-4" tabindex="0" role="region" aria-label="Канбан-доска воронки продаж">
<KanbanColumn
v-for="status in leadStatuses"
:key="status.slug"
@@ -325,6 +364,71 @@ defineExpose({
padding-bottom: 12px;
}
/* Телефон: переключатель этапов (чипсы) + одна колонка на всю ширину */
.kanban-mobile {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
}
.stage-switcher {
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 10px;
margin-bottom: 4px;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.stage-switcher::-webkit-scrollbar {
display: none;
}
.stage-chip {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 7px;
padding: 8px 14px;
border-radius: 999px;
border: 1px solid #d9d5cd;
background: #fff;
color: #3a3a33;
font-size: 13px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition:
border-color 150ms ease,
background 150ms ease;
}
.stage-chip__count {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-feature-settings: 'tnum';
font-size: 12px;
color: #66635c;
background: #f0ede4;
padding: 0 7px;
border-radius: 10px;
}
.stage-chip--active {
border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 12%, #fff);
color: #081319;
box-shadow: inset 0 -2px 0 var(--accent);
}
.stage-chip--active .stage-chip__count {
background: #fff;
}
/* Колонка на телефоне — во всю ширину (перебиваем фикс 280px из KanbanColumn) */
.kanban-mobile :deep(.kanban-column) {
flex: 1 1 auto;
width: 100%;
max-height: none;
}
.kanban-mobile :deep(.column-body) {
max-height: calc(100vh - 340px);
}
.kanban-board::-webkit-scrollbar {
height: 8px;
}