Skip to content

UX-паттерны

Формы и валидация

Zod-схемы

Валидация форм реализована через Zod-схемы, определённые в слое shared/schemas/ и entities/*/model/.

Ключевые схемы:

ФайлСхемы
shared/schemas/auth.tsloginSchema, registerSchema, forgotPasswordSchema, resetPasswordSchema, verifyEmailSchema
shared/schemas/settings.tsprofileFormSchema, changePasswordSchema, businessFormSchema
entities/car/model/car.schema.tsСхемы автомобилей (марка, модель, поколение, модификация)
entities/product/model/product.schema.tsСхемы товара, изображений, совместимости
entities/geo/model/geo.schema.tsСхемы регионов, городов, районов

Пример --- валидация пароля (единая для регистрации и смены пароля):

  • Минимум 8 символов
  • Обязательна заглавная, строчная, цифра, спецсимвол
  • Запрет 3+ повторяющихся и 3+ последовательных символов

Отображение ошибок

Используется два уровня:

  1. Ошибки полей --- через пропс error компонента UFormField:

    vue
    <UFormField :label="t('listing.title')" :error="fieldErrors['title']?.[0]">
      <UInput v-model="form.title" />
    </UFormField>
  2. Глобальные ошибки --- через UAlert:

    vue
    <UAlert v-if="error" color="error" :description="error" />

Обработка API-ошибок

Композабл useApiError (features/auth/composables/useApiError.ts) централизует обработку:

  • 422 (Validation) --- парсит error.details и раскладывает по полям.
  • 429 (Rate limit) --- показывает сообщение с расчётом времени из Retry-After.
  • Остальные ошибки --- показывает переведённое сообщение из словаря ERROR_TRANSLATIONS.

Паттерн формы (product-form)

Форма создания/редактирования объявления (features/product-form/) --- наиболее сложная. Использует:

  • Композабл useProductForm --- состояние формы, dirty-tracking, сохранение черновика, публикация.
  • Композабл useProductFormGuards --- защита от ухода без сохранения (onBeforeRouteLeave + beforeunload).
  • Композабл useProductFormPricing --- форматирование ввода цены.
  • Композабл useProductFormOEM --- управление списком OEM-номеров.
  • UTooltip на кнопке "Опубликовать" --- показывает причину, почему кнопка недоступна.
  • Модалка подтверждения ухода с несохранёнными изменениями (UModal).

Пагинация

Курсорная пагинация (useCursorPagination)

Файл: shared/api/useCursorPagination.ts

Единственный паттерн пагинации в проекте --- курсорный (не offset-based). Используется для списков с подгрузкой.

API-ответ:

ts
interface ApiListResponse<T> {
  data: T[]
  meta: {
    has_more: boolean
    next_cursor: string | null
  }
}

Композабл возвращает:

Поле/методОписание
itemsReadonly ref с накопленными элементами
hasMoreЕсть ли ещё данные
isLoadingИдёт ли загрузка
errorОшибка последнего запроса
refresh()Сброс и загрузка с начала
loadMore()Подгрузка следующей порции

Паттерн использования --- кнопка "Загрузить ещё" или infinite scroll.

Состояния загрузки

Глобальный загрузочный экран (AppLoadingScreen)

Файлы:

  • shared/ui/AppLoadingScreen.vue --- компонент
  • shared/ui/useLoadingScreen.ts --- composable

Полноэкранный оверлей с пульсирующим логотипом (AppLogo). Два режима:

  1. Splash (initial load) --- показывается при первом открытии, скрывается после app:suspense:resolve.
  2. Route transition --- показывается если навигация длится дольше 300ms (через хуки page:start/page:finish). Быстрые переходы не вызывают оверлей.

Фон адаптируется под тему (bg-white / dark:bg-gray-950). Fade-out 300ms через <Transition>.

Подключается в app.vue обоих приложений (main + admin).

Кнопки

Стандартный Nuxt UI-паттерн --- пропс :loading:

vue
<UButton :loading="saving" @click="onSave">Сохранить</UButton>

Селекторы

Каскадные селекторы (YMM, Geo, Category) используют :loading и :disabled:

vue
<USelectMenu
  :loading="geo.loadingCities.value"
  :disabled="!geo.selectedRegionId.value"
/>

Контентные области

Спиннер через UIcon с классом animate-spin:

vue
<UIcon name="i-lucide-loader-2" class="animate-spin" />

Изображения

При загрузке/ошибке в ImageUploader и MessageBubble используются overlay-спиннеры:

vue
<div class="absolute inset-0 flex items-center justify-center bg-black/40">
  <UIcon name="i-lucide-loader-2" class="h-8 w-8 animate-spin text-white" />
</div>

Модалки и диалоги

Используется UModal из Nuxt UI. Типичная структура слотов:

vue
<UModal v-model:open="isOpen">
  <template #header>Заголовок</template>
  <template #body>Контент</template>
  <template #footer>
    <div class="flex justify-end gap-3">
      <UButton variant="ghost" @click="isOpen = false">Отмена</UButton>
      <UButton color="primary" @click="confirm">Подтвердить</UButton>
    </div>
  </template>
</UModal>

Типичные сценарии модалок

КомпонентНазначение
AppHeaderПодтверждение выхода
ProductFormPageПредупреждение о несохранённых изменениях (3 кнопки: отмена, сбросить, сохранить)
ModerationActionsВвод причины отклонения
ReferenceFormModalУниверсальная форма создания/редактирования справочника
ReferenceListПодтверждение удаления
UserBlockActionПодтверждение блокировки/разблокировки
PhoneManagerДобавление/редактирование/удаление телефонов
CitySelectorМодалка выбора города
ChatImageLightboxПолноэкранный просмотр изображений (fullscreen)

USlideover

Используется для мобильного меню в AppHeader:

vue
<USlideover v-model:open="mobileMenuOpen" side="right" :title="t('common.menu')">

Уведомления (Toast)

Используется useToast() из Nuxt UI:

ts
const toast = useToast()
toast.add({ title: t('settings.phoneSaved'), color: 'success' })

Применяется для подтверждения действий (сохранение/удаление телефона в настройках).

Каскадные селекторы

Повторяющийся UI-паттерн: цепочка зависимых USelectMenu, где каждый следующий загружается и разблокируется после выбора предыдущего.

КомпонентУровни
YmmSelectМарка -> Модель -> Поколение -> Модификация
GeoSelectРегион -> Город -> Район -> Метро
CategoryCascadeSelectКатегория -> Подкатегория -> ... (N уровней)

YmmMultiSelect позволяет добавлять несколько строк YmmSelect для указания совместимости запчасти с разными автомобилями.

Поиск

Автокомплит (SearchBar / HeroSearchBar)

Два варианта строки поиска с единой логикой (useSearchAutocomplete):

  • SearchBar --- компактный, в хедере.
  • HeroSearchBar --- крупный (size="xl"), на главной странице.

Паттерн: debounce-ввод -> GET /store/products/suggest -> dropdown с результатами (ProductSuggestion: миниатюра + название + цена) -> клик ведёт на карточку товара или submit перенаправляет в каталог с ?q=....

При submit в каталог сортировка автоматически переключается на relevance (по релевантности). При очистке поискового запроса сортировка возвращается на date_desc.

Поиск в чате

Два уровня:

  • ChatSearchBar --- поиск внутри диалога (навигация по совпадениям стрелками, подсветка в тексте).
  • GlobalSearchResults --- глобальный поиск по всем диалогам (результаты группируются по диалогам).

Избранное

Кнопка FavoriteButton --- сердечко с toggle-логикой через favoritesStore. Показывается только авторизованным пользователям. Анимация: active:scale-90 при нажатии.

Product page

Composable: app/features/product-breadcrumbs/composables/useProductBreadcrumbs.ts

Breadcrumbs на странице товара строятся из цепочки part-категорий:

Главная > Запчасти > Двигатель > Прокладки ГБЦ > Прокладки ГБЦ Lada Niva
  • Используется <UBreadcrumb> из Nuxt UI
  • Цепочка строится из partCategoryChain (walk up parent_id tree)
  • Каждая категория ведёт на /catalog?category_id=...
  • Последний элемент (название товара) --- без ссылки, muted text
  • Пустой productTitle не рендерится (guard)

Защита маршрутов

  • Middleware auth.ts --- редирект на /auth/login для неавторизованных.
  • Middleware admin.ts --- редирект для не-админов.
  • Middleware guest.ts --- редирект авторизованных со страниц авторизации.

Страница ошибки

Файл: app/error.vue

Единая страница ошибки с различием 404 / 500:

  • SVG-иллюстрация (Storyset).
  • Заголовок и описание через i18n.
  • Кнопка "На главную" (clearError({ redirect: '/' })).

Dark mode

Переключение через UColorModeButton в хедере (desktop и mobile). Preference: system с fallback на light.

Локализация

  • Единственная локаль: ru.
  • Стратегия: no_prefix (без /ru/ в URL).
  • Ключи переводов в i18n/locales/ru.json.
  • Все тексты UI вынесены в i18n, хардкод минимален (только Zod-схемы с русскими сообщениями).