Appearance
UX-паттерны
Формы и валидация
Zod-схемы
Валидация форм реализована через Zod-схемы, определённые в слое shared/schemas/ и entities/*/model/.
Ключевые схемы:
| Файл | Схемы |
|---|---|
shared/schemas/auth.ts | loginSchema, registerSchema, forgotPasswordSchema, resetPasswordSchema, verifyEmailSchema |
shared/schemas/settings.ts | profileFormSchema, changePasswordSchema, businessFormSchema |
entities/car/model/car.schema.ts | Схемы автомобилей (марка, модель, поколение, модификация) |
entities/product/model/product.schema.ts | Схемы товара, изображений, совместимости |
entities/geo/model/geo.schema.ts | Схемы регионов, городов, районов |
Пример --- валидация пароля (единая для регистрации и смены пароля):
- Минимум 8 символов
- Обязательна заглавная, строчная, цифра, спецсимвол
- Запрет 3+ повторяющихся и 3+ последовательных символов
Отображение ошибок
Используется два уровня:
Ошибки полей --- через пропс
errorкомпонентаUFormField:vue<UFormField :label="t('listing.title')" :error="fieldErrors['title']?.[0]"> <UInput v-model="form.title" /> </UFormField>Глобальные ошибки --- через
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
}
}Композабл возвращает:
| Поле/метод | Описание |
|---|---|
items | Readonly ref с накопленными элементами |
hasMore | Есть ли ещё данные |
isLoading | Идёт ли загрузка |
error | Ошибка последнего запроса |
refresh() | Сброс и загрузка с начала |
loadMore() | Подгрузка следующей порции |
Паттерн использования --- кнопка "Загрузить ещё" или infinite scroll.
Состояния загрузки
Глобальный загрузочный экран (AppLoadingScreen)
Файлы:
shared/ui/AppLoadingScreen.vue--- компонентshared/ui/useLoadingScreen.ts--- composable
Полноэкранный оверлей с пульсирующим логотипом (AppLogo). Два режима:
- Splash (initial load) --- показывается при первом открытии, скрывается после
app:suspense:resolve. - 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 при нажатии.
Breadcrumbs
Product page
Composable: app/features/product-breadcrumbs/composables/useProductBreadcrumbs.ts
Breadcrumbs на странице товара строятся из цепочки part-категорий:
Главная > Запчасти > Двигатель > Прокладки ГБЦ > Прокладки ГБЦ Lada Niva- Используется
<UBreadcrumb>из Nuxt UI - Цепочка строится из
partCategoryChain(walk upparent_idtree) - Каждая категория ведёт на
/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-схемы с русскими сообщениями).