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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user