fix(desktop): каталог и фильтры заказов по канону + общий RefreshButton
- Каталог: чип-фильтр категорий + плавающая сортировка → канон PageTabs (категории как вкладки со счётчиком) + BaseSelect в слоте #actions. - «Мои заказы»: вкладки покрывают ВСЕ статусы заказа (был баг — заказ в SUPPLY_PREPARED не попадал ни в одну вкладку, кроме «Все»). Добавлена вкладка «Готовятся» (ACCEPTED/SUPPLY_PREPARED/ACCEPTED_TO_COOP), «Отменены» расширена истёкшими циклами и возвратами. - «Входящие заказы» поставщика: аналогичное исчерпывающее покрытие статусов. - Кнопка «Обновить» вынесена в общий виджет widgets/Marketplace/RefreshButton (дублировалась ~15× по столам, по-разному свёрстана) и поставлена в шапку: на «Сводный заказ» и «Готово к получению» — через канон Teleport в #header-actions-host, у поставщика — в слот вкладок PageTabs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+38
-55
@@ -9,6 +9,7 @@ import {
|
||||
} from 'src/widgets/Marketplace/CatalogOfferCard';
|
||||
import { BaseSelect, BaseButton, EmptyState } from 'src/shared/ui/base';
|
||||
import { PageHint } from 'src/shared/ui/domain';
|
||||
import { PageTabs, type PageTab } from 'src/shared/ui/layout';
|
||||
import { marketplaceUnitShort } from 'src/shared/lib/consts';
|
||||
import { marketplaceOfferImageUrls } from 'src/shared/lib/utils';
|
||||
import {
|
||||
@@ -49,6 +50,25 @@ const totalActiveCount = computed(() =>
|
||||
Array.from(counts.value.values()).reduce((acc, v) => acc + v, 0)
|
||||
);
|
||||
|
||||
// Категории как канон-вкладки (`PageTabs` со счётчиком). `key` — строковый id
|
||||
// категории (или 'all'), счётчик — число активных предложений в категории.
|
||||
const categoryTabs = computed<PageTab[]>(() => [
|
||||
{ key: 'all', label: 'Все', count: totalActiveCount.value },
|
||||
...categories.value.map((cat) => ({
|
||||
key: String(cat.id),
|
||||
label: cat.display_name,
|
||||
count: counts.value.get(cat.id) ?? 0,
|
||||
})),
|
||||
]);
|
||||
|
||||
const activeCategoryKey = computed(() =>
|
||||
selectedCategoryId.value === ALL_KEY ? 'all' : String(selectedCategoryId.value)
|
||||
);
|
||||
|
||||
function onSelectCategory(tab: PageTab): void {
|
||||
selectCategory(tab.key === 'all' ? ALL_KEY : Number(tab.key));
|
||||
}
|
||||
|
||||
const sortOptions: Array<{ label: string; value: CatalogSort }> = [
|
||||
{ label: 'Свежие сначала', value: 'created_at_desc' },
|
||||
{ label: 'Цена ↑', value: 'price_asc' },
|
||||
@@ -159,37 +179,18 @@ q-page.catalog(role="region", aria-label="Каталог Стола заказо
|
||||
PageHint(storage-key="mp:catalog:banner-dismissed")
|
||||
| Предложения поставщиков кооператива. Выберите товар или оформите заказ на ваш пункт выдачи.
|
||||
|
||||
.catalog__toolbar
|
||||
.catalog__filters(role="tablist", aria-label="Фильтр по категориям")
|
||||
.chip.catalog__chip(
|
||||
:class="selectedCategoryId === ALL_KEY ? 'chip--accent' : 'chip--neutral'",
|
||||
role="tab",
|
||||
:aria-selected="selectedCategoryId === ALL_KEY",
|
||||
tabindex="0",
|
||||
@click="selectCategory(ALL_KEY)",
|
||||
@keydown.enter="selectCategory(ALL_KEY)"
|
||||
PageTabs.catalog__tabs(
|
||||
:tabs="categoryTabs",
|
||||
:active-key="activeCategoryKey",
|
||||
@select="onSelectCategory"
|
||||
)
|
||||
template(#actions)
|
||||
BaseSelect.catalog__sort(
|
||||
:model-value="sort",
|
||||
:options="sortOptions",
|
||||
label="Сортировка",
|
||||
@update:model-value="onSortChange"
|
||||
)
|
||||
span Все
|
||||
span.catalog__count {{ totalActiveCount }}
|
||||
.chip.catalog__chip(
|
||||
v-for="cat in categories",
|
||||
:key="cat.id",
|
||||
:class="selectedCategoryId === cat.id ? 'chip--accent' : 'chip--neutral'",
|
||||
role="tab",
|
||||
:aria-selected="selectedCategoryId === cat.id",
|
||||
tabindex="0",
|
||||
@click="selectCategory(cat.id)",
|
||||
@keydown.enter="selectCategory(cat.id)"
|
||||
)
|
||||
span {{ cat.display_name }}
|
||||
span.catalog__count {{ counts.get(cat.id) ?? 0 }}
|
||||
q-space
|
||||
BaseSelect.catalog__sort(
|
||||
:model-value="sort",
|
||||
:options="sortOptions",
|
||||
label="Сортировка",
|
||||
@update:model-value="onSortChange"
|
||||
)
|
||||
|
||||
q-inner-loading(:showing="loading && items.length === 0")
|
||||
q-spinner(color="primary", size="2em")
|
||||
@@ -232,32 +233,14 @@ q-page.catalog(role="region", aria-label="Каталог Стола заказо
|
||||
flex-direction: column;
|
||||
gap: var(--p-4, 16px);
|
||||
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--p-3, 12px);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
// Канон-`.tabbar` тянется во всю ширину; гасим его внутренний горизонтальный
|
||||
// паддинг, чтобы вкладки шли от края страницы (у страницы свои отступы).
|
||||
&__tabs {
|
||||
margin: 0 calc(-1 * var(--p-6, 24px));
|
||||
|
||||
&__filters {
|
||||
display: flex;
|
||||
gap: var(--p-2, 8px);
|
||||
overflow-x: auto;
|
||||
flex-wrap: nowrap;
|
||||
padding-bottom: var(--p-1, 4px);
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__chip {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__count {
|
||||
color: var(--p-ink-3);
|
||||
font-variant-numeric: tabular-nums;
|
||||
:deep(.tabbar__tabs) {
|
||||
padding: 0 var(--p-6, 24px);
|
||||
}
|
||||
}
|
||||
|
||||
&__sort {
|
||||
|
||||
@@ -33,9 +33,13 @@ const activeKey = ref('all');
|
||||
|
||||
const hasMore = computed(() => currentPage.value < totalPages.value);
|
||||
|
||||
// Фильтр по этапу. Вкладка может покрывать несколько статусов: «Ждут
|
||||
// поставщика» — и сводные, и индивидуальные заказы (оба ждут акцепта
|
||||
// поставщика), иначе индивидуальный заказ в неё не попадал.
|
||||
// Фильтр по этапу. Покрытие ИСЧЕРПЫВАЮЩЕЕ по enum'у MarketplaceOrderStatusView:
|
||||
// каждый статус заказа попадает хотя бы в одну вкладку. Иначе заказ молча
|
||||
// «растекается» — пропадает из всех вкладок, кроме «Все» (баг: молоко в статусе
|
||||
// SUPPLY_PREPARED не показывалось нигде). Вкладка может покрывать несколько
|
||||
// статусов: «Ждут поставщика» — сводные и индивидуальные (оба ждут акцепта);
|
||||
// «Готовятся» — принят → готовится → принят кооперативом (для заказчика это
|
||||
// единый этап «в работе»); «Отменены» — отмены, истёкшие циклы и возвраты.
|
||||
const FILTERS: Array<{ key: string; label: string; statuses: MarketplaceOrderStatusView[] | null }> = [
|
||||
{ key: 'all', label: 'Все', statuses: null },
|
||||
{ key: 'active', label: 'Активные', statuses: ['ACTIVE'] },
|
||||
@@ -44,13 +48,23 @@ const FILTERS: Array<{ key: string; label: string; statuses: MarketplaceOrderSta
|
||||
label: 'Ждут поставщика',
|
||||
statuses: ['ACCEPTED_PENDING_SUPPLIER', 'ACCEPTED_PENDING_SUPPLIER_INDIVIDUAL'],
|
||||
},
|
||||
{ key: 'accepted', label: 'Приняты', statuses: ['ACCEPTED'] },
|
||||
{
|
||||
key: 'in-progress',
|
||||
label: 'Готовятся',
|
||||
statuses: ['ACCEPTED', 'SUPPLY_PREPARED', 'ACCEPTED_TO_COOP'],
|
||||
},
|
||||
{ key: 'ready', label: 'Готовы к выдаче', statuses: ['READY_TO_RECEIVE'] },
|
||||
{ key: 'received', label: 'Получены', statuses: ['RECEIVED'] },
|
||||
{
|
||||
key: 'cancelled',
|
||||
key: 'closed',
|
||||
label: 'Отменены',
|
||||
statuses: ['CANCELLED_BY_ORDERER', 'CANCELLED_BY_SUPPLIER'],
|
||||
statuses: [
|
||||
'CANCELLED_BY_ORDERER',
|
||||
'CANCELLED_BY_SUPPLIER',
|
||||
'EXPIRED_NO_THRESHOLD',
|
||||
'EXPIRED_NO_VOLUME',
|
||||
'RETURNED',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+23
-11
@@ -10,6 +10,7 @@ import {
|
||||
toOrderCardModel,
|
||||
type Order as OrderCardModel,
|
||||
} from 'src/widgets/Marketplace/OrderCard';
|
||||
import { RefreshButton } from 'src/widgets/Marketplace/RefreshButton';
|
||||
import {
|
||||
acceptConsolidatedRequest,
|
||||
acceptIndividualOrder,
|
||||
@@ -59,6 +60,11 @@ const showSkeleton = computed(() => loading.value && items.value.length === 0);
|
||||
// фильтровала только сводный статус, и индивидуальный заказ с кнопкой
|
||||
// «Принять» в неё не попадал (отдельная вкладка «Индивидуальные» путала).
|
||||
// `key` — стабильный слаг в URL (`?status=pending-accept`) для прямых ссылок.
|
||||
// Покрытие исчерпывающее по enum'у MarketplaceOrderStatusView: каждый статус
|
||||
// заказа попадает хотя бы в одну вкладку, иначе при фильтрации заказ молча
|
||||
// «растекается» — пропадает из всех вкладок, кроме «Все». READY_TO_RECEIVE для
|
||||
// поставщика — продолжение «у кооператива»; EXPIRED_*/RETURNED — терминальные
|
||||
// вместе с отменами.
|
||||
const FILTERS: Array<{ key: string; label: string; statuses: MarketplaceOrderStatusView[] | null }> = [
|
||||
{ key: 'all', label: 'Все', statuses: null },
|
||||
{
|
||||
@@ -68,9 +74,23 @@ const FILTERS: Array<{ key: string; label: string; statuses: MarketplaceOrderSta
|
||||
},
|
||||
{ key: 'accepted', label: 'Приняты', statuses: ['ACCEPTED'] },
|
||||
{ key: 'supply-prepared', label: 'Поставка готова', statuses: ['SUPPLY_PREPARED'] },
|
||||
{ key: 'accepted-to-coop', label: 'Приняты кооперативом', statuses: ['ACCEPTED_TO_COOP'] },
|
||||
{
|
||||
key: 'accepted-to-coop',
|
||||
label: 'У кооператива',
|
||||
statuses: ['ACCEPTED_TO_COOP', 'READY_TO_RECEIVE'],
|
||||
},
|
||||
{ key: 'received', label: 'Получены', statuses: ['RECEIVED'] },
|
||||
{ key: 'cancelled', label: 'Отменены', statuses: ['CANCELLED_BY_ORDERER', 'CANCELLED_BY_SUPPLIER'] },
|
||||
{
|
||||
key: 'closed',
|
||||
label: 'Отменены',
|
||||
statuses: [
|
||||
'CANCELLED_BY_ORDERER',
|
||||
'CANCELLED_BY_SUPPLIER',
|
||||
'EXPIRED_NO_THRESHOLD',
|
||||
'EXPIRED_NO_VOLUME',
|
||||
'RETURNED',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const tabs = computed<PageTab[]>(() => FILTERS.map((f) => ({ key: f.key, label: f.label })));
|
||||
@@ -216,15 +236,7 @@ q-page.incoming-orders(role='region', aria-label='Входящие заказы
|
||||
|
||||
PageTabs.incoming-orders__tabs(:tabs='tabs', :active-key='activeKey', @select='onSelectTab')
|
||||
template(#actions)
|
||||
BaseButton(
|
||||
variant='ghost',
|
||||
icon-only,
|
||||
aria-label='Обновить',
|
||||
:loading='loading',
|
||||
@click='load(1, false)'
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name='refresh', size='20px')
|
||||
RefreshButton(:loading='loading', @refresh='load(1, false)')
|
||||
|
||||
//- Скелетон вместо спиннера — каркас карточек проявляется сразу.
|
||||
.row.q-col-gutter-md(v-if='showSkeleton')
|
||||
|
||||
+6
-12
@@ -6,7 +6,8 @@ import {
|
||||
orderStatusDisplay,
|
||||
toOrderCardModel,
|
||||
} from 'src/widgets/Marketplace/OrderCard';
|
||||
import { BaseButton, EmptyState } from 'src/shared/ui/base';
|
||||
import { RefreshButton } from 'src/widgets/Marketplace/RefreshButton';
|
||||
import { EmptyState } from 'src/shared/ui/base';
|
||||
import { PageHint } from 'src/shared/ui/domain';
|
||||
import { fetchMyOrders } from '../../MyOrders/api';
|
||||
import type {
|
||||
@@ -169,15 +170,13 @@ onUnmounted(() => {
|
||||
|
||||
<template lang="pug">
|
||||
q-page.consolidated(role="region", aria-label="Сводный заказ")
|
||||
//- Действие страницы — в шапку, где стоят общие действия (канон Teleport).
|
||||
Teleport(to="#header-actions-host", defer)
|
||||
RefreshButton(:loading="loading", @refresh="load")
|
||||
|
||||
PageHint(storage-key="mp:consolidated:banner-dismissed")
|
||||
| Партии заказов, сгруппированные по циклу. Несколько ваших заказов в одной партии обслуживаются совместно — на одном цикле, с одной поставкой.
|
||||
|
||||
.consolidated__toolbar
|
||||
q-space
|
||||
BaseButton(variant="ghost", iconOnly, ariaLabel="Обновить", :loading="loading", @click="load")
|
||||
template(#icon-left)
|
||||
q-icon(name="refresh", size="18px")
|
||||
|
||||
q-inner-loading(:showing="loading && items.length === 0")
|
||||
q-spinner(color="primary", size="2em")
|
||||
|
||||
@@ -236,11 +235,6 @@ q-page.consolidated(role="region", aria-label="Сводный заказ")
|
||||
flex-direction: column;
|
||||
gap: var(--p-4, 16px);
|
||||
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
+6
-13
@@ -2,7 +2,8 @@
|
||||
import type { QTableProps } from 'quasar';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { FailAlert } from 'src/shared/api';
|
||||
import { BaseButton, EmptyState } from 'src/shared/ui/base';
|
||||
import { EmptyState } from 'src/shared/ui/base';
|
||||
import { RefreshButton } from 'src/widgets/Marketplace/RefreshButton';
|
||||
import { PageHint } from 'src/shared/ui/domain';
|
||||
import { marketplaceUnitShort } from 'src/shared/lib/consts/marketplace-units';
|
||||
import { listMyReadyToReceive, type MarketplaceOrderIssuanceView } from '../api';
|
||||
@@ -77,16 +78,13 @@ onMounted(load);
|
||||
|
||||
<template lang="pug">
|
||||
q-page.ready(role="region", aria-label="Готово к получению")
|
||||
//- Действие страницы — в шапку, где стоят общие действия (канон Teleport).
|
||||
Teleport(to="#header-actions-host", defer)
|
||||
RefreshButton(:loading="loading", @refresh="load")
|
||||
|
||||
PageHint(storage-key="mp:ready-to-receive:banner-dismissed")
|
||||
| Оператор открыл выдачу этих заказов на пункте. Приходите на участок для сверки имущества и финальной подписи.
|
||||
|
||||
.ready__toolbar
|
||||
q-space
|
||||
BaseButton(variant="ghost", :loading="loading", @click="load")
|
||||
template(#icon-left)
|
||||
q-icon(name="refresh", size="18px")
|
||||
| Обновить
|
||||
|
||||
q-table(
|
||||
:rows="items",
|
||||
:columns="columns",
|
||||
@@ -110,11 +108,6 @@ q-page.ready(role="region", aria-label="Готово к получению")
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--p-4, 16px);
|
||||
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<template lang="pug">
|
||||
BaseButton(
|
||||
variant='ghost',
|
||||
icon-only,
|
||||
:aria-label='ariaLabel',
|
||||
:loading='loading',
|
||||
@click="emit('refresh')"
|
||||
)
|
||||
template(#icon-left)
|
||||
q-icon(name='refresh', :size='size')
|
||||
q-tooltip(anchor='bottom middle', self='top middle') {{ ariaLabel }}
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BaseButton } from 'src/shared/ui/base';
|
||||
|
||||
/**
|
||||
* Канон-кнопка «Обновить» для столов Marketplace. Вынесена в общий виджет —
|
||||
* ручная перезагрузка ленты повторялась в ~15 страницах столов (входящие
|
||||
* заказы, сводный заказ, готово к получению, склад и т.д.), каждый раз слегка
|
||||
* по-разному (то icon-only, то с подписью, разный размер иконки) и «выпадала»
|
||||
* из общего вида. Единый ghost-icon согласует внешний вид и место (под-навигация
|
||||
* `PageTabs #actions` либо шапка через `Teleport to="#header-actions-host"`).
|
||||
*/
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
/** Идёт ли загрузка — крутит спиннер вместо иконки. */
|
||||
loading?: boolean;
|
||||
/** Размер иконки (по умолчанию 20px — как у глобальных действий шапки). */
|
||||
size?: string;
|
||||
/** Подпись для скринридера и подсказки. */
|
||||
ariaLabel?: string;
|
||||
}>(),
|
||||
{ loading: false, size: '20px', ariaLabel: 'Обновить' },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{ refresh: [] }>();
|
||||
</script>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as RefreshButton } from './RefreshButton.vue';
|
||||
Reference in New Issue
Block a user